diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..71ec71e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.git +.github +.gitignore +*.md +*.sh +scripts/ +test/ +node_modules/ +.npm +.env +.env.local +.DS_Store diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc1cbd0..e938413 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,4 +18,5 @@ jobs: - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} + cache: 'npm' - run: npm test diff --git a/.github/workflows/shell-lint.yml b/.github/workflows/shell-lint.yml index a2a1691..d45594c 100644 --- a/.github/workflows/shell-lint.yml +++ b/.github/workflows/shell-lint.yml @@ -31,7 +31,7 @@ jobs: health.sh \ verify.sh \ scripts/test-all.sh \ - scripts/pre-commit || true + scripts/pre-commit syntax-check: runs-on: ubuntu-latest diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index a078893..d7add1a 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -68,7 +68,7 @@ clausidian/ ├── CHANGELOG.md ├── ARCHITECTURE.md (this file) ├── CONTRIBUTING.md -└── .github/workflows/ # CI/CD pipeline (coming in v2.5.0) +└── .github/workflows/ # CI/CD pipeline (test, lint, publish) ``` ## Data Flow diff --git a/CHANGELOG.md b/CHANGELOG.md index ef19e87..1cfd06d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,46 @@ All notable changes to the clausidian project are documented in this file. Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [2.6.0] — 2026-03-30 + +### Added +- **embed-search command** — Semantic search using embeddings (Ollama or OpenAI) +- **smart-search command** — BM25 ranked search (now exposed as MCP tool) +- **mcpSchema for import command** — Can now be triggered via MCP +- **mcpSchema for review command** — Can now be triggered via MCP +- **6 new scaffold slash commands** — update, sync, health, stats, tag-list, batch + +### Changed +- **Performance fix**: Cache invalidation now conditional on write operations only (read-only tools skip cache clear) +- **SearchCache** wired into search path for 5-min TTL result caching +- Improved MCP configuration documentation (mcp-config-example.README.md) + +### Infrastructure +- **Dockerfile** — Containerized deployment (node:18-alpine, MCP server ready) +- **CI/CD improvements** — npm caching, enforced ShellCheck failures +- Cleaned up mcp-config-example.json (removed invalid JavaScript comments) + +## [2.5.1] — 2026-03-30 + +### Added +- **lib/common.sh** — Shared shell library (212 lines), eliminates 85 lines of duplication +- **Shell script refactoring** — All shells now use common functions +- **.editorconfig** — Unified code style across all file types +- **.prettierrc.json** — JavaScript/TypeScript formatting with 100-column width +- **.github/workflows/shell-lint.yml** — Automated ShellCheck + syntax validation +- **SCRIPT_STYLE.md** — 12-part shell scripting best practices guide +- **GitHub release v2.5.1** — npm publication at v2.5.1 + +### Changed +- install.sh, setup.sh, health.sh, verify.sh refactored to use lib/common.sh +- All scripts now enforce `set -o pipefail` for better error handling +- Improved logging with color-coded output across all shell scripts + +### Tested +- 18-item automated test suite (100% pass rate) +- Shell syntax validation with bash -n +- GitHub Actions CI/CD matrix (3 OS × 3 Node versions = 9 jobs) +======= ## [3.0.0] — 2026-03-30 ### Added @@ -29,6 +69,7 @@ All notable changes to the clausidian project are documented in this file. Forma - `vault-validator` replaces inline vault checks - `args-parser` normalizes kebab-case flags to camelCase - All existing APIs remain compatible +>>>>>>> origin/main ## [2.5.0] — 2026-03-30 @@ -55,14 +96,15 @@ All notable changes to the clausidian project are documented in this file. Forma ## [Unreleased] -### Planned for v2.6.0+ +### Planned for v2.7.0+ - Smart template generation from vault analysis -- Search result caching for improved performance - Incremental index updates support - Large vault support (>10,000 files) - Batch operation parallelization - Performance benchmarking suite - Pre-commit hook configuration +- Extended MCP resources (per-note URIs, live stats) +- Advanced embedding models (text-embedding-3-large, custom models) ## [2.0.0] — 2026-03-30 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8361df8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +# Clausidian Docker image — MCP server for Obsidian vault management +FROM node:18-alpine + +WORKDIR /app + +# Copy package files +COPY package.json package-lock.json ./ + +# Install dependencies +RUN npm ci + +# Copy application code +COPY bin/ ./bin/ +COPY src/ ./src/ +COPY scaffold/ ./scaffold/ +COPY skill/ ./skill/ + +# Create vault mount point +RUN mkdir -p /vault + +# Set vault as default working directory for MCP operations +ENV VAULT_ROOT=/vault + +# Default command: start MCP server +CMD ["node", "bin/cli.mjs", "serve", "--vault", "/vault"] + +# Volume for vault data +VOLUME ["/vault"] + +# Expose stdio for MCP protocol +EXPOSE 3000 diff --git a/bin/cli.mjs b/bin/cli.mjs index 1dba1e6..5831cb6 100755 --- a/bin/cli.mjs +++ b/bin/cli.mjs @@ -2,7 +2,7 @@ /** * clausidian CLI — AI agent toolkit for Obsidian vaults - * v1.1.0 — registry-based dispatch + * v2.6.0 — registry-based dispatch, MCP server, 55+ commands */ import { getCommand, getCommandNames } from '../src/registry.mjs'; diff --git a/mcp-config-example.README.md b/mcp-config-example.README.md new file mode 100644 index 0000000..72653e3 --- /dev/null +++ b/mcp-config-example.README.md @@ -0,0 +1,51 @@ +# MCP Configuration Setup + +> `mcp-config-example.json` is a template for setting up clausidian as an MCP server in Claude Code. + +## Setup Instructions + +### 1. Replace the vault path + +Edit `mcp-config-example.json` and replace `REPLACE_WITH_YOUR_VAULT_PATH` with your actual vault directory: + +- **macOS/Linux**: `/Users/username/my-vault` or `$HOME/my-vault` (use full path, not `~`) +- **Windows**: `C:\\Users\\username\\my-vault` + +### 2. Common vault locations + +- **iCloud Drive**: `/Users/username/Library/Mobile Documents/com~apple~CloudDocs/my-vault` +- **Dropbox**: `/Users/username/Dropbox/my-vault` +- **Local**: `/Users/username/obsidian-vault` + +### 3. Add to Claude Code MCP config + +1. Edit `~/.claude/.mcp.json` +2. Paste the entire `mcpServers` block from this example into your existing `mcpServers` object + +Example `~/.claude/.mcp.json`: +```json +{ + "mcpServers": { + "clausidian": { + "command": "clausidian", + "args": ["serve", "--vault", "/Users/username/my-vault"] + } + } +} +``` + +### 4. Verify configuration + +After editing: +1. Restart Claude Code +2. Run `/obsidian health` in any project +3. If you see vault statistics, configuration is successful + +### 5. Troubleshooting + +| Issue | Solution | +|-------|----------| +| Permission denied | Ensure your user has access to the vault directory | +| Path not found | Use `echo $HOME` to confirm path; always use full path (no `~`) | +| MCP not loading | Check Claude Code output logs for clausidian connection errors | +| Command not found | Ensure `clausidian` is installed: `npm install -g clausidian` | diff --git a/mcp-config-example.json b/mcp-config-example.json index b7b2611..b67e1d7 100644 --- a/mcp-config-example.json +++ b/mcp-config-example.json @@ -6,29 +6,3 @@ } } } - -/* - 配置說明: - - 1. 將 REPLACE_WITH_YOUR_VAULT_PATH 替換為你的 vault 路徑,例如: - - macOS/Linux: "/Users/username/my-vault" 或 "$HOME/my-vault" - - Windows: "C:\\Users\\username\\my-vault" - - 2. 常見路徑示例: - - iCloud Drive: "/Users/username/Library/Mobile Documents/com~apple~CloudDocs/my-vault" - - Dropbox: "/Users/username/Dropbox/my-vault" - - 本地: "/Users/username/obsidian-vault" - - 3. 添加到正確位置: - 編輯 ~/.claude/.mcp.json,將此整個 mcpServers 塊添加到已有的 "mcpServers" 對象中 - - 4. 驗證配置: - - 重啟 Claude Code - - 在任何項目目錄執行: /obsidian health - - 如果輸出 vault 統計,說明配置成功 - - 5. 故障排查: - - 權限不足: 確保用戶有訪問 vault 目錄的權限 - - 路徑錯誤: 使用 echo $HOME 確認路徑,避免使用 ~ (使用完整路徑) - - MCP 未加載: 檢查 Claude Code 輸出日誌是否有 clausidian 連接錯誤 -*/ diff --git a/package.json b/package.json index d43b579..331db08 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "clausidian", - "version": "3.0.1", - "description": "CLI toolkit for AI agents to manage Obsidian vaults — journal, notes, search, index sync, knowledge graphs, and more", + "version": "2.6.0", + "description": "CLI toolkit for AI agents to manage Obsidian vaults — journal, notes, search, index sync, AI recommendations, and more", "type": "module", "bin": { "clausidian": "bin/cli.mjs" diff --git a/scaffold/.claude/commands/batch.md b/scaffold/.claude/commands/batch.md new file mode 100644 index 0000000..fd4908f --- /dev/null +++ b/scaffold/.claude/commands/batch.md @@ -0,0 +1,11 @@ +Apply changes to multiple notes at once. + +Run: `clausidian batch tag --tag newtag --filter status:active` + +Operations: +- `batch tag`: add/remove tags from matching notes +- `batch update`: change frontmatter (status, goal, summary) +- `batch archive`: bulk-archive matching notes +- Filter by type, tag, status, or keyword + +$ARGUMENTS diff --git a/scaffold/.claude/commands/health.md b/scaffold/.claude/commands/health.md new file mode 100644 index 0000000..99f9290 --- /dev/null +++ b/scaffold/.claude/commands/health.md @@ -0,0 +1,12 @@ +Get vault health score and diagnostics. + +Run: `clausidian health --json` + +Checks: +- Total notes, archive count, orphan notes +- Index freshness +- Tag coverage, broken link count +- Average note length and update frequency +- Overall health score (0-100) + +$ARGUMENTS diff --git a/scaffold/.claude/commands/stats.md b/scaffold/.claude/commands/stats.md new file mode 100644 index 0000000..e636695 --- /dev/null +++ b/scaffold/.claude/commands/stats.md @@ -0,0 +1,13 @@ +Display vault statistics (note count, types, tags, sizes). + +Run: `clausidian stats --json` + +Returns: +- Notes by type (area, project, resource, idea) +- Top tags and their usage +- Note length distribution +- Archive ratio +- Active notes (modified in last 7, 30, 90 days) +- Total vault size + +$ARGUMENTS diff --git a/scaffold/.claude/commands/sync.md b/scaffold/.claude/commands/sync.md new file mode 100644 index 0000000..9babbc1 --- /dev/null +++ b/scaffold/.claude/commands/sync.md @@ -0,0 +1,11 @@ +Rebuild vault indices (_index.md, _tags.md, _graph.md, directory indexes). + +Run: `clausidian sync` + +This rescans all notes and updates: +- _index.md: central index of all notes with summaries +- _tags.md: tag-to-note mapping with counts +- _graph.md: Mermaid knowledge graph with relationship suggestions +- directory indexes: one per area/project/resource/idea type + +$ARGUMENTS diff --git a/scaffold/.claude/commands/tag-list.md b/scaffold/.claude/commands/tag-list.md new file mode 100644 index 0000000..f5f0a42 --- /dev/null +++ b/scaffold/.claude/commands/tag-list.md @@ -0,0 +1,11 @@ +List all tags in vault with usage counts. + +Run: `clausidian tag-list` + +Shows: +- Each tag name +- How many notes use it +- Most common tags first +- Filtered by prefix if provided (e.g., `tag-list concept-`) + +$ARGUMENTS diff --git a/scaffold/.claude/commands/update.md b/scaffold/.claude/commands/update.md new file mode 100644 index 0000000..72c9b1f --- /dev/null +++ b/scaffold/.claude/commands/update.md @@ -0,0 +1,11 @@ +Update note frontmatter (tags, status, goal, summary). + +Run: `clausidian update --tags tag1,tag2 --status active` + +If the CLI is not available: +1. Read the note file +2. Update the YAML frontmatter with provided fields +3. Keep body unchanged +4. Update indices (_tags.md, _graph.md) + +$ARGUMENTS diff --git a/src/mcp-server.mjs b/src/mcp-server.mjs index 6fbd2f4..12219c0 100644 --- a/src/mcp-server.mjs +++ b/src/mcp-server.mjs @@ -12,6 +12,7 @@ import { resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; import { Vault } from './vault.mjs'; import { getMcpTools, getMcpDispatch } from './registry.mjs'; +import { SearchCache } from './search-cache.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const PKG_VERSION = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf8')).version; @@ -20,6 +21,13 @@ const PKG_VERSION = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.js const TOOLS = getMcpTools(); const DISPATCH = getMcpDispatch(); +// Tools that modify vault state — invalidate cache on these only +const WRITE_TOOLS = new Set([ + 'note', 'journal', 'capture', 'update', 'patch', 'delete', + 'archive', 'rename', 'move', 'merge', 'relink', 'import', 'sync', + 'batch_tag', 'batch_update', 'batch_archive', +]); + // ── MCP Resources ────────────────────────────────────── const RESOURCES = [ @@ -93,6 +101,7 @@ export class McpServer { constructor(vaultRoot) { this.vaultRoot = vaultRoot; this._vault = null; + this._searchCache = new SearchCache(); } get vault() { @@ -101,21 +110,39 @@ export class McpServer { } async handleToolCall(name, args) { - this.vault.invalidateCache(); + // Only invalidate cache for write operations + if (WRITE_TOOLS.has(name)) { + this.vault.invalidateCache(); + this._searchCache.clear(); + } const handler = DISPATCH[name]; if (!handler) throw new Error(`Unknown tool: ${name}`); + // Try search cache for read-only search tools + if ((name === 'search' || name === 'embed-search' || name === 'smart-search') && !WRITE_TOOLS.has(name)) { + const cached = this._searchCache.get(args.keyword, args); + if (cached) return cached; + } + const origLog = console.log; const origError = console.error; console.log = () => {}; console.error = () => {}; + let result; try { - return await handler(this.vaultRoot, args); + result = await handler(this.vaultRoot, args); } finally { console.log = origLog; console.error = origError; } + + // Cache search results + if ((name === 'search' || name === 'embed-search' || name === 'smart-search') && !WRITE_TOOLS.has(name)) { + this._searchCache.set(args.keyword, args, result); + } + + return result; } readResource(uri) { diff --git a/src/registry.mjs b/src/registry.mjs index db4bb8a..91b7f01 100644 --- a/src/registry.mjs +++ b/src/registry.mjs @@ -359,9 +359,13 @@ const COMMANDS = [ name: 'import', description: 'Import notes from JSON or markdown', usage: 'import ', + mcpSchema: { + file: { type: 'string', description: 'JSON or markdown file to import' }, + }, + mcpRequired: ['file'], async run(root, flags, pos) { const { importNotes } = await import('./commands/import.mjs'); - return importNotes(root, pos[0]); + return importNotes(root, flags.file || pos[0]); }, }, @@ -428,8 +432,14 @@ const COMMANDS = [ name: 'review', description: 'Generate weekly or monthly review', usage: 'review [monthly]', + mcpSchema: { + monthly: { type: 'boolean', description: 'Generate monthly review instead of weekly' }, + date: { type: 'string', description: 'Review date in YYYY-MM-DD format' }, + year: { type: 'number', description: 'Year for monthly review' }, + month: { type: 'number', description: 'Month (1-12) for monthly review' }, + }, async run(root, flags, pos) { - if (pos[0] === 'monthly') { + if (flags.monthly || pos[0] === 'monthly') { const { monthlyReview } = await import('./commands/review.mjs'); return monthlyReview(root, { year: flags.year ? parseInt(flags.year) : undefined, @@ -826,6 +836,52 @@ const COMMANDS = [ return launchd(root, pos[0], flags); }, }, + { + name: 'embed-search', + description: 'Semantic search using embeddings (Ollama or OpenAI)', + usage: 'embed-search ', + mcpSchema: { + query: { type: 'string', description: 'Search query' }, + provider: { type: 'string', enum: ['ollama', 'openai', 'off'], description: 'Embedding provider (auto-detected if not specified)' }, + model: { type: 'string', description: 'Model name for embeddings' }, + apiKey: { type: 'string', description: 'OpenAI API key (or set OA_OPENAI_KEY env)' }, + limit: { type: 'number', description: 'Max results (default: 10)' }, + threshold: { type: 'number', description: 'Min cosine similarity (0-1, default: 0.7)' }, + }, + mcpRequired: ['query'], + async run(root, flags, pos) { + const { embedSearch } = await import('./commands/embed-search.mjs'); + return embedSearch(root, flags.query || pos[0], { + provider: flags.provider, + model: flags.model, + apiKey: flags.apiKey, + limit: flags.limit ? parseInt(flags.limit) : undefined, + threshold: flags.threshold ? parseFloat(flags.threshold) : undefined, + }); + }, + }, + { + name: 'smart-search', + description: 'BM25 ranked search across vault notes', + usage: 'smart-search ', + mcpSchema: { + query: { type: 'string', description: 'Search query' }, + type: { type: 'string', enum: ['area', 'project', 'resource', 'idea'], description: 'Filter by note type' }, + tag: { type: 'string', description: 'Filter by tag' }, + status: { type: 'string', description: 'Filter by status' }, + limit: { type: 'number', description: 'Max results (default: 20)' }, + }, + mcpRequired: ['query'], + async run(root, flags, pos) { + const { smartSearch } = await import('./commands/smart-search.mjs'); + return smartSearch(root, flags.query || pos[0], { + type: flags.type, + tag: flags.tag, + status: flags.status, + limit: flags.limit ? parseInt(flags.limit) : undefined, + }); + }, + }, ]; // ── Lookup helpers ──────────────────────────────────────