commit 08cc6e9ea60389c6b0340c7045eb1d29c81fffe2 Author: Bill Date: Thu Oct 16 20:52:52 2025 -0400 Release v1.0.0 - Initial Release ๐ŸŽ‰ Initial release of Obsidian MCP Server plugin Core Features: - MCP server implementation with HTTP transport - JSON-RPC 2.0 message handling - Protocol version 2024-11-05 support MCP Tools: - read_note, create_note, update_note, delete_note - search_notes, list_notes, get_vault_info Server Features: - Configurable HTTP server (default port: 3000) - Health check and MCP endpoints - Auto-start option Security: - Origin header validation (DNS rebinding protection) - Optional Bearer token authentication - CORS configuration UI: - Settings panel with full configuration - Status bar indicator and ribbon icon - Start/Stop/Restart commands Documentation: - Comprehensive README with examples - Quick Start Guide and Implementation Summary - Test client script diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..84b8a66 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +# top-most EditorConfig file +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = tab +indent_size = 4 +tab_width = 4 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..e019f3c --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +node_modules/ + +main.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..0807290 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,23 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "env": { "node": true }, + "plugins": [ + "@typescript-eslint" + ], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], + "parserOptions": { + "sourceType": "module" + }, + "rules": { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], + "@typescript-eslint/ban-ts-comment": "off", + "no-prototype-builtins": "off", + "@typescript-eslint/no-empty-function": "off" + } + } \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e09a007 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# vscode +.vscode + +# Intellij +*.iml +.idea + +# npm +node_modules + +# Don't include the compiled main.js file in the repo. +# They should be uploaded to GitHub releases instead. +main.js + +# Exclude sourcemaps +*.map + +# obsidian +data.json + +# Exclude macOS Finder (System Explorer) View States +.DS_Store diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..b973752 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +tag-version-prefix="" \ No newline at end of file diff --git a/.windsurf/rules/agent-guidelines.md b/.windsurf/rules/agent-guidelines.md new file mode 100644 index 0000000..a3747f1 --- /dev/null +++ b/.windsurf/rules/agent-guidelines.md @@ -0,0 +1,26 @@ +--- +description: Agent-specific do's and don'ts +--- + +# Agent Guidelines + +## Do + +- Add commands with stable IDs (don't rename once released) +- Provide defaults and validation in settings +- Write idempotent code paths so reload/unload doesn't leak listeners or intervals +- Use `this.register*` helpers for everything that needs cleanup +- Keep `main.ts` minimal and focused on lifecycle management +- Split functionality across separate modules +- Organize code into logical folders (commands/, ui/, utils/) + +## Don't + +- Introduce network calls without an obvious user-facing reason and documentation +- Ship features that require cloud services without clear disclosure and explicit opt-in +- Store or transmit vault contents unless essential and consented +- Put all code in `main.ts` - delegate to separate modules +- Create files larger than 200-300 lines without splitting them +- Commit build artifacts to version control +- Change plugin `id` after release +- Rename command IDs after release diff --git a/.windsurf/rules/code-examples.md b/.windsurf/rules/code-examples.md new file mode 100644 index 0000000..061fcdd --- /dev/null +++ b/.windsurf/rules/code-examples.md @@ -0,0 +1,84 @@ +--- +trigger: always_on +description: Common code patterns and examples +--- + +# Code Examples + +## Organize Code Across Multiple Files + +### main.ts (minimal, lifecycle only) + +```ts +import { Plugin } from "obsidian"; +import { MySettings, DEFAULT_SETTINGS } from "./settings"; +import { registerCommands } from "./commands"; + +export default class MyPlugin extends Plugin { + settings: MySettings; + + async onload() { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + registerCommands(this); + } +} +``` + +### settings.ts + +```ts +export interface MySettings { + enabled: boolean; + apiKey: string; +} + +export const DEFAULT_SETTINGS: MySettings = { + enabled: true, + apiKey: "", +}; +``` + +### commands/index.ts + +```ts +import { Plugin } from "obsidian"; +import { doSomething } from "./my-command"; + +export function registerCommands(plugin: Plugin) { + plugin.addCommand({ + id: "do-something", + name: "Do something", + callback: () => doSomething(plugin), + }); +} +``` + +## Add a Command + +```ts +this.addCommand({ + id: "your-command-id", + name: "Do the thing", + callback: () => this.doTheThing(), +}); +``` + +## Persist Settings + +```ts +interface MySettings { enabled: boolean } +const DEFAULT_SETTINGS: MySettings = { enabled: true }; + +async onload() { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + await this.saveData(this.settings); +} +``` + +## Register Listeners Safely + +```ts +this.registerEvent(this.app.workspace.on("file-open", f => { /* ... */ })); +this.registerDomEvent(window, "resize", () => { /* ... */ }); +this.registerInterval(window.setInterval(() => { /* ... */ }, 1000)); +``` \ No newline at end of file diff --git a/.windsurf/rules/coding-conventions.md b/.windsurf/rules/coding-conventions.md new file mode 100644 index 0000000..e2f91c2 --- /dev/null +++ b/.windsurf/rules/coding-conventions.md @@ -0,0 +1,35 @@ +--- +trigger: always_on +description: TypeScript coding conventions and best practices +--- + +# Coding Conventions + +## TypeScript Standards + +- Use TypeScript with `"strict": true` preferred +- Bundle everything into `main.js` (no unbundled runtime deps) +- Prefer `async/await` over promise chains +- Handle errors gracefully + +## Code Organization + +- **Keep `main.ts` minimal** - Focus only on plugin lifecycle (onload, onunload, addCommand calls) +- **Delegate all feature logic to separate modules** +- **Split large files** - If any file exceeds ~200-300 lines, break it into smaller, focused modules +- **Use clear module boundaries** - Each file should have a single, well-defined responsibility + +## Platform Compatibility + +- Avoid Node/Electron APIs if you want mobile compatibility +- Set `isDesktopOnly` accordingly if using desktop-only features +- Test on iOS and Android where feasible +- Don't assume desktop-only behavior unless `isDesktopOnly` is `true` + +## Performance + +- Keep startup light - defer heavy work until needed +- Avoid long-running tasks during `onload` - use lazy initialization +- Batch disk access and avoid excessive vault scans +- Debounce/throttle expensive operations in response to file system events +- Avoid large in-memory structures on mobile - be mindful of memory and storage constraints \ No newline at end of file diff --git a/.windsurf/rules/commands-settings.md b/.windsurf/rules/commands-settings.md new file mode 100644 index 0000000..2f1307a --- /dev/null +++ b/.windsurf/rules/commands-settings.md @@ -0,0 +1,54 @@ +--- +trigger: always_on +description: Commands and settings implementation guidelines +--- + +# Commands & Settings + +## Commands + +- Add user-facing commands via `this.addCommand(...)` +- **Use stable command IDs** - Don't rename once released +- Ensure commands are unique and descriptive + +### Example: Add a Command + +```ts +this.addCommand({ + id: "your-command-id", + name: "Do the thing", + callback: () => this.doTheThing(), +}); +``` + +## Settings + +- Provide a settings tab if the plugin has configuration +- Always provide sensible defaults +- Persist settings using `this.loadData()` / `this.saveData()` +- Provide defaults and validation in settings + +### Example: Persist Settings + +```ts +interface MySettings { enabled: boolean } +const DEFAULT_SETTINGS: MySettings = { enabled: true }; + +async onload() { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + await this.saveData(this.settings); +} +``` + +## Resource Management + +- Write idempotent code paths so reload/unload doesn't leak listeners or intervals +- Use `this.register*` helpers for everything that needs cleanup + +### Example: Register Listeners Safely + +```ts +this.registerEvent(this.app.workspace.on("file-open", f => { /* ... */ })); +this.registerDomEvent(window, "resize", () => { /* ... */ }); +this.registerInterval(window.setInterval(() => { /* ... */ }, 1000)); +``` \ No newline at end of file diff --git a/.windsurf/rules/environment-tooling.md b/.windsurf/rules/environment-tooling.md new file mode 100644 index 0000000..8c2f5f5 --- /dev/null +++ b/.windsurf/rules/environment-tooling.md @@ -0,0 +1,38 @@ +--- +trigger: always_on +description: Development environment and tooling requirements +--- + +# Environment & Tooling + +## Required Tools + +- **Node.js**: Use current LTS (Node 18+ recommended) +- **Package manager**: npm (required for this sample - `package.json` defines npm scripts and dependencies) +- **Bundler**: esbuild (required for this sample - `esbuild.config.mjs` and build scripts depend on it) +- **Types**: `obsidian` type definitions + +**Note**: This sample project has specific technical dependencies on npm and esbuild. If creating a plugin from scratch, you can choose different tools, but you'll need to replace the build configuration accordingly. Alternative bundlers like Rollup or webpack are acceptable if they bundle all external dependencies into `main.js`. + +## Common Commands + +### Install dependencies +```bash +npm install +``` + +### Development (watch mode) +```bash +npm run dev +``` + +### Production build +```bash +npm run build +``` + +## Linting + +- Install eslint: `npm install -g eslint` +- Analyze project: `eslint main.ts` +- Analyze folder: `eslint ./src/` \ No newline at end of file diff --git a/.windsurf/rules/file-organization.md b/.windsurf/rules/file-organization.md new file mode 100644 index 0000000..2dcdf3a --- /dev/null +++ b/.windsurf/rules/file-organization.md @@ -0,0 +1,39 @@ +--- +trigger: always_on +description: File and folder organization conventions +--- + +# File & Folder Organization + +## Core Principles + +- **Organize code into multiple files**: Split functionality across separate modules rather than putting everything in `main.ts` +- **Keep `main.ts` minimal**: Focus only on plugin lifecycle (onload, onunload, addCommand calls) +- **Split large files**: If any file exceeds ~200-300 lines, break it into smaller, focused modules +- **Use clear module boundaries**: Each file should have a single, well-defined responsibility + +## Recommended Structure + +``` +src/ + main.ts # Plugin entry point, lifecycle management only + settings.ts # Settings interface and defaults + commands/ # Command implementations + command1.ts + command2.ts + ui/ # UI components, modals, views + modal.ts + view.ts + utils/ # Utility functions, helpers + helpers.ts + constants.ts + types.ts # TypeScript interfaces and types +``` + +## Best Practices + +- Source lives in `src/` +- Keep the plugin small - avoid large dependencies +- Prefer browser-compatible packages +- Generated output should be placed at the plugin root or `dist/` depending on build setup +- Release artifacts must end up at the top level of the plugin folder (`main.js`, `manifest.json`, `styles.css`) \ No newline at end of file diff --git a/.windsurf/rules/manifest-rules.md b/.windsurf/rules/manifest-rules.md new file mode 100644 index 0000000..c63388b --- /dev/null +++ b/.windsurf/rules/manifest-rules.md @@ -0,0 +1,30 @@ +--- +trigger: always_on +description: Manifest.json requirements and conventions +--- + +# Manifest Rules + +## Required Fields + +The `manifest.json` must include: + +- `id` - Plugin ID; for local dev it should match the folder name +- `name` - Display name +- `version` - Semantic Versioning `x.y.z` +- `minAppVersion` - Minimum Obsidian version required +- `description` - Brief description +- `isDesktopOnly` - Boolean indicating mobile compatibility + +## Optional Fields + +- `author` - Plugin author name +- `authorUrl` - Author's URL +- `fundingUrl` - Funding/donation URL (string or map) + +## Critical Rules + +- **Never change `id` after release** - Treat it as stable API +- Keep `minAppVersion` accurate when using newer APIs +- Use Semantic Versioning for `version` field +- Canonical requirements: https://github.com/obsidianmd/obsidian-releases/blob/master/.github/workflows/validate-plugin-entry.yml \ No newline at end of file diff --git a/.windsurf/rules/project-overview.md b/.windsurf/rules/project-overview.md new file mode 100644 index 0000000..4ab2dc9 --- /dev/null +++ b/.windsurf/rules/project-overview.md @@ -0,0 +1,16 @@ +--- +trigger: always_on +description: Obsidian plugin project structure and requirements +--- + +# Project Overview + +- **Target**: Obsidian Community Plugin (TypeScript โ†’ bundled JavaScript) +- **Entry point**: `main.ts` compiled to `main.js` and loaded by Obsidian +- **Required release artifacts**: `main.js`, `manifest.json`, and optional `styles.css` + +## Key Requirements + +- All TypeScript code must be bundled into a single `main.js` file +- Release artifacts must be placed at the top level of the plugin folder +- Never commit build artifacts (`node_modules/`, `main.js`, etc.) to version control \ No newline at end of file diff --git a/.windsurf/rules/references.md b/.windsurf/rules/references.md new file mode 100644 index 0000000..37aa439 --- /dev/null +++ b/.windsurf/rules/references.md @@ -0,0 +1,22 @@ +--- +trigger: always_on +description: Official documentation and reference links +--- + +# References + +## Official Resources + +- **Obsidian sample plugin**: https://github.com/obsidianmd/obsidian-sample-plugin +- **API documentation**: https://docs.obsidian.md +- **Developer policies**: https://docs.obsidian.md/Developer+policies +- **Plugin guidelines**: https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines +- **Style guide**: https://help.obsidian.md/style-guide +- **Manifest validation**: https://github.com/obsidianmd/obsidian-releases/blob/master/.github/workflows/validate-plugin-entry.yml + +## When to Consult + +- Check **Developer policies** before implementing features that access external services +- Review **Plugin guidelines** before submitting to the community catalog +- Reference **API documentation** when using Obsidian APIs +- Follow **Style guide** for UI text and documentation \ No newline at end of file diff --git a/.windsurf/rules/security-privacy.md b/.windsurf/rules/security-privacy.md new file mode 100644 index 0000000..76f05a0 --- /dev/null +++ b/.windsurf/rules/security-privacy.md @@ -0,0 +1,27 @@ +--- +trigger: always_on +description: Security, privacy, and compliance requirements +--- + +# Security, Privacy, and Compliance + +Follow Obsidian's **Developer Policies** and **Plugin Guidelines**. + +## Network & External Services + +- **Default to local/offline operation** - Only make network requests when essential to the feature +- **No hidden telemetry** - If you collect optional analytics or call third-party services, require explicit opt-in and document clearly in `README.md` and in settings +- **Never execute remote code** - Don't fetch and eval scripts, or auto-update plugin code outside of normal releases +- **Clearly disclose external services** - Document any external services used, data sent, and risks + +## Data Access & Privacy + +- **Minimize scope** - Read/write only what's necessary inside the vault +- **Do not access files outside the vault** +- **Respect user privacy** - Do not collect vault contents, filenames, or personal information unless absolutely necessary and explicitly consented +- **No deceptive patterns** - Avoid ads or spammy notifications + +## Resource Management + +- **Register and clean up all resources** - Use the provided `register*` helpers so the plugin unloads safely +- Clean up DOM, app, and interval listeners properly \ No newline at end of file diff --git a/.windsurf/rules/troubleshooting.md b/.windsurf/rules/troubleshooting.md new file mode 100644 index 0000000..3567ea7 --- /dev/null +++ b/.windsurf/rules/troubleshooting.md @@ -0,0 +1,45 @@ +--- +trigger: always_on +description: Common issues and solutions +--- + +# Troubleshooting + +## Plugin Doesn't Load After Build + +**Issue**: Plugin doesn't appear in Obsidian after building + +**Solution**: Ensure `main.js` and `manifest.json` are at the top level of the plugin folder under `/.obsidian/plugins//` + +## Build Issues + +**Issue**: `main.js` is missing after build + +**Solution**: Run `npm run build` or `npm run dev` to compile your TypeScript source code + +## Commands Not Appearing + +**Issue**: Commands don't show up in command palette + +**Solution**: +- Verify `addCommand` runs after `onload` +- Ensure command IDs are unique +- Check that commands are properly registered + +## Settings Not Persisting + +**Issue**: Settings reset after reloading Obsidian + +**Solution**: +- Ensure `loadData`/`saveData` are awaited +- Re-render the UI after changes +- Verify settings are properly merged with defaults + +## Mobile-Only Issues + +**Issue**: Plugin works on desktop but not mobile + +**Solution**: +- Confirm you're not using desktop-only APIs +- Check `isDesktopOnly` setting in manifest +- Test on actual mobile devices or adjust compatibility \ No newline at end of file diff --git a/.windsurf/rules/ux-guidelines.md b/.windsurf/rules/ux-guidelines.md new file mode 100644 index 0000000..293e205 --- /dev/null +++ b/.windsurf/rules/ux-guidelines.md @@ -0,0 +1,32 @@ +--- +trigger: always_on +description: UX and copy guidelines for UI text +--- + +# UX & Copy Guidelines + +For UI text, commands, and settings: + +## Text Formatting + +- **Prefer sentence case** for headings, buttons, and titles +- Use clear, action-oriented imperatives in step-by-step copy +- Keep in-app strings short, consistent, and free of jargon + +## UI References + +- Use **bold** to indicate literal UI labels +- Prefer "select" for interactions +- Use arrow notation for navigation: **Settings โ†’ Community plugins** + +## Examples + +โœ… Good: +- "Select **Settings โ†’ Community plugins**" +- "Enable the plugin" +- "Configure your API key" + +โŒ Avoid: +- "Go to Settings and then Community plugins" +- "Turn on the plugin" +- "Setup your API key" \ No newline at end of file diff --git a/.windsurf/rules/versioning-releases.md b/.windsurf/rules/versioning-releases.md new file mode 100644 index 0000000..3f3de6b --- /dev/null +++ b/.windsurf/rules/versioning-releases.md @@ -0,0 +1,32 @@ +--- +trigger: always_on +description: Versioning and release process +--- + +# Versioning & Releases + +## Version Management + +- Bump `version` in `manifest.json` using Semantic Versioning (SemVer) +- Update `versions.json` to map plugin version โ†’ minimum app version +- Keep version numbers consistent across all release artifacts + +## Release Process + +1. **Create GitHub release** with tag that exactly matches `manifest.json`'s `version` + - **Do not use a leading `v`** in the tag +2. **Attach required assets** to the release: + - `manifest.json` + - `main.js` + - `styles.css` (if present) +3. After initial release, follow the process to add/update your plugin in the community catalog + +## Testing Before Release + +Manual install for testing: +1. Copy `main.js`, `manifest.json`, `styles.css` (if any) to: + ``` + /.obsidian/plugins// + ``` +2. Reload Obsidian +3. Enable the plugin in **Settings โ†’ Community plugins** \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..66cabb3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,163 @@ +# Changelog + +All notable changes to the Obsidian MCP Server plugin will be documented in this file. + +## [1.0.0] - 2025-10-16 + +### ๐ŸŽ‰ Initial Release + +#### Added + +**Core Features** +- MCP (Model Context Protocol) server implementation +- HTTP transport with Express.js +- JSON-RPC 2.0 message handling +- Protocol version 2024-11-05 support + +**MCP Tools** +- `read_note` - Read note content from vault +- `create_note` - Create new notes +- `update_note` - Update existing notes +- `delete_note` - Delete notes +- `search_notes` - Search notes by content or filename +- `list_notes` - List all notes or notes in specific folder +- `get_vault_info` - Get vault metadata and statistics + +**Server Features** +- Configurable HTTP server (default port: 3000) +- Localhost-only binding (127.0.0.1) +- Health check endpoint (`/health`) +- MCP endpoint (`/mcp`) +- Auto-start option + +**Security** +- Origin header validation (DNS rebinding protection) +- Optional Bearer token authentication +- CORS configuration with allowed origins +- Request validation and error handling + +**User Interface** +- Settings panel with full configuration +- Status bar indicator showing server state +- Ribbon icon for quick server toggle +- Start/Stop/Restart commands +- Real-time server status display +- Connection information display + +**Documentation** +- Comprehensive README with examples +- Quick Start Guide +- Implementation Summary +- Test client script +- Example MCP requests +- Security considerations + +**Developer Tools** +- TypeScript implementation +- esbuild bundler +- Test client for validation +- Health check endpoint + +### Technical Details + +**Dependencies** +- express: ^4.18.2 +- cors: ^2.8.5 +- obsidian: latest + +**Build** +- TypeScript 4.7.4 +- esbuild 0.17.3 +- Output: 828KB bundled + +**Compatibility** +- Obsidian minimum version: 0.15.0 +- Desktop only (requires Node.js HTTP server) +- Protocol: MCP 2024-11-05 + +### Known Limitations + +- Desktop only (not available on mobile) +- Single vault per server instance +- No WebSocket support (HTTP only) +- No SSL/TLS (localhost only) + +--- + +## Future Roadmap + +See [ROADMAP.md](ROADMAP.md) for detailed implementation plans. + +### Phase 1: Foundation (P0-P1) +- **Path Normalization** - Consistent path handling across platforms +- **Error Message Improvements** - Clear, actionable error messages with troubleshooting tips +- **Enhanced Authentication** - Secure API key management, multiple keys with labels, expiration, rate limiting, audit logging, and permission scopes +- **API Unification** - Standardize parameter naming and return structured, typed results +- **Discovery Endpoints** - Add `stat` and `exists` tools for exploring vault structure + +### Phase 2: Enhanced Operations (P1-P2) +- **Write Operations & Concurrency** - ETag-based version control, partial updates (frontmatter, sections) +- **Conflict Resolution** - Create notes with conflict strategies (error, overwrite, rename) +- **File Rename/Move** - Rename or move files with automatic wikilink updates +- **Enhanced List Operations** - Filtering, recursion control, pagination, frontmatter summaries +- **Advanced Search** - Regex search, snippet extraction, glob filtering + +### Phase 3: Advanced Features (P2-P3) +- **Frontmatter Parsing** - Read and update frontmatter without modifying content +- **Linking & Backlinks** - Wikilink validation, resolution, and backlink queries +- **Waypoint Support** - Tools for working with Waypoint plugin markers +- **Excalidraw Support** - Specialized tool for reading Excalidraw drawings + +### Future Considerations +- **Resources API** - Expose notes as MCP resources +- **Prompts API** - Templated prompts for common operations +- **Batch Operations** - Multiple operations in single request +- **WebSocket Transport** - Real-time updates and notifications +- **Graph API** - Enhanced graph visualization and traversal +- **Tag & Canvas APIs** - Query tags and manipulate canvas files +- **Dataview Integration** - Query vault using Dataview syntax +- **Performance Enhancements** - Indexing, caching, streaming for large vaults + +--- + +## Version History + +| Version | Date | Notes | +|---------|------|-------| +| 1.0.0 | 2025-10-16 | Initial release | + +--- + +## Upgrade Guide + +### From Development to 1.0.0 + +If you were using a development version: + +1. Backup your settings +2. Disable the plugin +3. Delete the old plugin folder +4. Install version 1.0.0 +5. Re-enable and reconfigure + +### Breaking Changes + +None (initial release) + +--- + +## Support + +For issues, questions, or contributions: +- Check the README.md for documentation +- Review QUICKSTART.md for setup help +- Check existing issues before creating new ones +- Include version number in bug reports + +--- + +## Credits + +- MCP Protocol: https://modelcontextprotocol.io +- Obsidian API: https://github.com/obsidianmd/obsidian-api +- Built with TypeScript, Express.js, and โค๏ธ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..287f37a --- /dev/null +++ b/LICENSE @@ -0,0 +1,5 @@ +Copyright (C) 2020-2025 by Dynalist Inc. + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. \ No newline at end of file diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..914f8b3 --- /dev/null +++ b/QUICKSTART.md @@ -0,0 +1,198 @@ +# Quick Start Guide + +## ๐Ÿš€ Getting Started + +### 1. Enable the Plugin + +1. Open Obsidian +2. Go to **Settings** โ†’ **Community Plugins** +3. Find **MCP Server** in the list +4. Toggle it **ON** + +### 2. Start the Server + +**Option A: Via Ribbon Icon** +- Click the server icon (๐Ÿ“ก) in the left sidebar + +**Option B: Via Command Palette** +- Press `Ctrl/Cmd + P` +- Type "Start MCP Server" +- Press Enter + +**Option C: Auto-start** +- Go to **Settings** โ†’ **MCP Server** +- Enable "Auto-start server" +- Server will start automatically when Obsidian launches + +### 3. Verify Server is Running + +Check the status bar at the bottom of Obsidian: +- **Running**: `MCP: Running (3000)` +- **Stopped**: `MCP: Stopped` + +Or visit: http://127.0.0.1:3000/health + +### 4. Test the Connection + +Run the test client: +```bash +node test-client.js +``` + +Expected output: +``` +๐Ÿงช Testing Obsidian MCP Server + +Server: http://127.0.0.1:3000/mcp +API Key: None + +1๏ธโƒฃ Testing initialize... +โœ… Initialize successful + Server: obsidian-mcp-server 1.0.0 + Protocol: 2024-11-05 + +2๏ธโƒฃ Testing tools/list... +โœ… Tools list successful + Found 7 tools: + - read_note: Read the content of a note from the Obsidian vault + - create_note: Create a new note in the Obsidian vault + ... + +๐ŸŽ‰ All tests passed! +``` + +## ๐Ÿ”ง Configuration + +### Basic Settings + +Go to **Settings** โ†’ **MCP Server**: + +| Setting | Default | Description | +|---------|---------|-------------| +| Port | 3000 | HTTP server port | +| Auto-start | Off | Start server on Obsidian launch | +| Enable CORS | On | Allow cross-origin requests | +| Allowed Origins | * | Comma-separated list of allowed origins | + +### Security Settings + +| Setting | Default | Description | +|---------|---------|-------------| +| Enable Authentication | Off | Require API key for requests | +| API Key | (empty) | Bearer token for authentication | + +## ๐Ÿ”Œ Connect an MCP Client + +### Claude Desktop + +Edit your Claude Desktop config file: + +**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` +**Windows**: `%APPDATA%\Claude\claude_desktop_config.json` + +Add: +```json +{ + "mcpServers": { + "obsidian": { + "url": "http://127.0.0.1:3000/mcp" + } + } +} +``` + +Restart Claude Desktop. + +### Other MCP Clients + +Use the endpoint: `http://127.0.0.1:3000/mcp` + +## ๐Ÿ“ Available Tools + +Once connected, you can use these tools: + +- **read_note** - Read note content +- **create_note** - Create a new note +- **update_note** - Update existing note +- **delete_note** - Delete a note +- **search_notes** - Search vault by query +- **list_notes** - List all notes or notes in a folder +- **get_vault_info** - Get vault metadata + +## ๐Ÿ”’ Using Authentication + +1. Enable authentication in settings +2. Set an API key (e.g., `my-secret-key-123`) +3. Include in requests: + +```bash +curl -X POST http://127.0.0.1:3000/mcp \ + -H "Authorization: Bearer my-secret-key-123" \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' +``` + +Or in Claude Desktop config: +```json +{ + "mcpServers": { + "obsidian": { + "url": "http://127.0.0.1:3000/mcp", + "headers": { + "Authorization": "Bearer my-secret-key-123" + } + } + } +} +``` + +## โ“ Troubleshooting + +### Server won't start + +**Error: Port already in use** +- Change the port in settings +- Or stop the process using port 3000 + +**Error: Cannot find module** +- Run `npm install` in the plugin directory +- Rebuild with `npm run build` + +### Cannot connect from client + +**Check server is running** +- Look at status bar: should show "MCP: Running (3000)" +- Visit http://127.0.0.1:3000/health + +**Check firewall** +- Ensure localhost connections are allowed +- Server only binds to 127.0.0.1 (localhost) + +**Check authentication** +- If enabled, ensure API key is correct +- Check Authorization header format + +### Tools not working + +**Path errors** +- Use relative paths from vault root +- Example: `folder/note.md` not `/full/path/to/note.md` + +**Permission errors** +- Ensure Obsidian has file system access +- Check vault is not read-only + +## ๐ŸŽฏ Next Steps + +- Read the full [README.md](README.md) for detailed documentation +- Explore the [MCP Protocol Documentation](https://modelcontextprotocol.io) +- Check example requests in the README +- Customize settings for your workflow + +## ๐Ÿ’ก Tips + +- Use the ribbon icon for quick server toggle +- Enable auto-start for seamless integration +- Use authentication for additional security +- Monitor the status bar for server state +- Check Obsidian console (Ctrl+Shift+I) for detailed logs diff --git a/README.md b/README.md new file mode 100644 index 0000000..c09ba46 --- /dev/null +++ b/README.md @@ -0,0 +1,255 @@ +# Obsidian MCP Server Plugin + +An Obsidian plugin that exposes your vault operations via the [Model Context Protocol (MCP)](https://modelcontextprotocol.io) over HTTP. This allows AI assistants and other MCP clients to interact with your Obsidian vault programmatically. + +## Features + +- **HTTP MCP Server**: Runs an HTTP server implementing the MCP protocol +- **Vault Operations**: Exposes tools for reading, creating, updating, and deleting notes +- **Search Functionality**: Search notes by content or filename +- **Security**: Localhost-only binding, optional authentication, CORS configuration +- **Easy Configuration**: Simple settings UI with server status and controls + +## Available MCP Tools + +- `read_note` - Read the content of a note +- `create_note` - Create a new note +- `update_note` - Update an existing note +- `delete_note` - Delete a note +- `search_notes` - Search for notes by query +- `list_notes` - List all notes or notes in a folder +- `get_vault_info` - Get vault metadata + +## Installation + +### From Source + +1. Clone this repository into your vault's plugins folder: + ```bash + cd /path/to/vault/.obsidian/plugins + git clone obsidian-mcp-server + cd obsidian-mcp-server + ``` + +2. Install dependencies: + ```bash + npm install + ``` + +3. Build the plugin: + ```bash + npm run build + ``` + +4. Enable the plugin in Obsidian Settings โ†’ Community Plugins + +## Configuration + +1. Open Obsidian Settings โ†’ MCP Server +2. Configure the following options: + - **Port**: HTTP server port (default: 3000) + - **Auto-start**: Automatically start server on Obsidian launch + - **Enable CORS**: Allow cross-origin requests + - **Allowed Origins**: Comma-separated list of allowed origins + - **Enable Authentication**: Require API key for requests + - **API Key**: Bearer token for authentication + +3. Click "Start Server" or use the ribbon icon to toggle the server + +## Usage + +### Starting the Server + +- **Via Ribbon Icon**: Click the server icon in the left sidebar +- **Via Command Palette**: Run "Start MCP Server" +- **Auto-start**: Enable in settings to start automatically + +### Connecting an MCP Client + +The server exposes an MCP endpoint at: +``` +http://127.0.0.1:3000/mcp +``` + +Example client configuration (e.g., for Claude Desktop): +```json +{ + "mcpServers": { + "obsidian": { + "url": "http://127.0.0.1:3000/mcp" + } + } +} +``` + +### Using with Authentication + +If authentication is enabled, include the API key in requests: +```bash +curl -X POST http://127.0.0.1:3000/mcp \ + -H "Authorization: Bearer YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' +``` + +## Example MCP Requests + +### Initialize Connection +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { + "name": "example-client", + "version": "1.0.0" + } + } +} +``` + +### List Available Tools +```json +{ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/list" +} +``` + +### Read a Note +```json +{ + "jsonrpc": "2.0", + "id": 3, + "method": "tools/call", + "params": { + "name": "read_note", + "arguments": { + "path": "folder/note.md" + } + } +} +``` + +### Create a Note +```json +{ + "jsonrpc": "2.0", + "id": 4, + "method": "tools/call", + "params": { + "name": "create_note", + "arguments": { + "path": "new-note.md", + "content": "# New Note\n\nThis is the content." + } + } +} +``` + +### Search Notes +```json +{ + "jsonrpc": "2.0", + "id": 5, + "method": "tools/call", + "params": { + "name": "search_notes", + "arguments": { + "query": "search term" + } + } +} +``` + +## Security Considerations + +- **Localhost Only**: The server binds to `127.0.0.1` to prevent external access +- **Origin Validation**: Validates request origins to prevent DNS rebinding attacks +- **Optional Authentication**: Use API keys to restrict access +- **Desktop Only**: This plugin only works on desktop (not mobile) due to HTTP server requirements + +## Development + +### Building from Source + +```bash +npm install +npm run dev # Watch mode for development +npm run build # Production build +``` +- Make changes to `main.ts` (or create new `.ts` files). Those changes should be automatically compiled into `main.js`. +- Reload Obsidian to load the new version of your plugin. +- Enable plugin in settings window. +- For updates to the Obsidian API run `npm update` in the command line under your repo folder. + +## Releasing new releases + +- Update your `manifest.json` with your new version number, such as `1.0.1`, and the minimum Obsidian version required for your latest release. +- Update your `versions.json` file with `"new-plugin-version": "minimum-obsidian-version"` so older versions of Obsidian can download an older version of your plugin that's compatible. +- Create new GitHub release using your new version number as the "Tag version". Use the exact version number, don't include a prefix `v`. See here for an example: https://github.com/obsidianmd/obsidian-sample-plugin/releases +- Upload the files `manifest.json`, `main.js`, `styles.css` as binary attachments. Note: The manifest.json file must be in two places, first the root path of your repository and also in the release. +- Publish the release. + +> You can simplify the version bump process by running `npm version patch`, `npm version minor` or `npm version major` after updating `minAppVersion` manually in `manifest.json`. +> The command will bump version in `manifest.json` and `package.json`, and add the entry for the new version to `versions.json` + +## Adding your plugin to the community plugin list + +- Check the [plugin guidelines](https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines). +- Publish an initial version. +- Make sure you have a `README.md` file in the root of your repo. +- Make a pull request at https://github.com/obsidianmd/obsidian-releases to add your plugin. + +## How to use + +- Clone this repo. +- Make sure your NodeJS is at least v16 (`node --version`). +- `npm i` or `yarn` to install dependencies. +- `npm run dev` to start compilation in watch mode. + +## Manually installing the plugin + +- Copy over `main.js`, `styles.css`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/your-plugin-id/`. + +## Improve code quality with eslint (optional) +- [ESLint](https://eslint.org/) is a tool that analyzes your code to quickly find problems. You can run ESLint against your plugin to find common bugs and ways to improve your code. +- To use eslint with this project, make sure to install eslint from terminal: + - `npm install -g eslint` +- To use eslint to analyze this project use this command: + - `eslint main.ts` + - eslint will then create a report with suggestions for code improvement by file and line number. +- If your source code is in a folder, such as `src`, you can use eslint with this command to analyze all files in that folder: + - `eslint ./src/` + +## Funding URL + +You can include funding URLs where people who use your plugin can financially support it. + +The simple way is to set the `fundingUrl` field to your link in your `manifest.json` file: + +```json +{ + "fundingUrl": "https://buymeacoffee.com" +} +``` + +If you have multiple URLs, you can also do: + +```json +{ + "fundingUrl": { + "Buy Me a Coffee": "https://buymeacoffee.com", + "GitHub Sponsor": "https://github.com/sponsors", + "Patreon": "https://www.patreon.com/" + } +} +``` + +## API Documentation + +See https://github.com/obsidianmd/obsidian-api diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..48641a2 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,1341 @@ +# Obsidian MCP Server - Development Roadmap + +**Version:** 1.0.0 +**Last Updated:** October 16, 2025 +**Status:** Planning Phase + +This roadmap outlines planned improvements and fixes for the Obsidian MCP Server plugin based on user feedback and testing of read-only tools. + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [Priority Matrix](#priority-matrix) +3. [Phase 1: Path Normalization & Error Handling](#phase-1-path-normalization--error-handling) +4. [Phase 2: API Unification & Typed Results](#phase-2-api-unification--typed-results) +5. [Phase 3: Discovery Endpoints](#phase-3-discovery-endpoints) +6. [Phase 4: Enhanced List Operations](#phase-4-enhanced-list-operations) +7. [Phase 5: Advanced Read Operations](#phase-5-advanced-read-operations) +8. [Phase 6: Powerful Search](#phase-6-powerful-search) +9. [Phase 7: Waypoint Support](#phase-7-waypoint-support) +10. [Phase 8: Write Operations & Concurrency](#phase-8-write-operations--concurrency) +11. [Phase 9: Linking & Backlinks](#phase-9-linking--backlinks) +12. [Testing & Documentation](#testing--documentation) +13. [Performance Considerations](#performance-considerations) + +--- + +## Overview + +The plugin is currently minimally functioning with basic CRUD operations and simple search. This roadmap focuses on: + +- **Robustness**: Better path handling across platforms +- **Discoverability**: New endpoints for exploring vault structure +- **Power**: Enhanced search and filtering capabilities +- **Consistency**: Unified API patterns and predictable behavior +- **UX**: Clear error messages with actionable guidance + +--- + +## Priority Matrix + +| Priority | Category | Estimated Effort | +|----------|----------|------------------| +| **P0** | Path Normalization | 1-2 days | +| **P0** | Error Message Improvements | 1 day | +| **P0** | Enhanced Authentication | 2-3 days | +| **P1** | API Unification | 2-3 days | +| **P1** | Typed Results | 1-2 days | +| **P1** | Discovery Endpoints | 2-3 days | +| **P1** | Write Operations & Concurrency | 5-6 days | +| **P2** | List Ergonomics | 3-4 days | +| **P2** | Enhanced Search | 4-5 days | +| **P2** | Linking & Backlinks | 3-4 days | +| **P3** | Advanced Read Operations | 2-3 days | +| **P3** | Waypoint Support | 3-4 days | + +**Total Estimated Effort:** 29-42 days + +--- + +## Phase 1: Path Normalization & Error Handling + +**Priority:** P0 +**Dependencies:** None +**Estimated Effort:** 2-3 days + +### Goals + +Ensure consistent path handling across Windows, macOS, and Linux, with clear error messages. + +### Tasks + +#### 1.1 Path Normalization Utility + +**File:** `path-utils.ts` (new) + +- [ ] Create utility module for path operations +- [ ] Implement `normalizePath(path: string): string` + - Strip leading/trailing slashes + - Convert backslashes to forward slashes + - Handle Windows drive letters + - Normalize case on Windows (case-insensitive) + - Preserve case on macOS/Linux (case-sensitive) +- [ ] Implement `isValidVaultPath(path: string): boolean` +- [ ] Implement `resolveVaultPath(app: App, path: string): TFile | TFolder | null` +- [ ] Add unit tests for path normalization + +#### 1.2 Update All Tool Implementations + +- [ ] Replace direct `getAbstractFileByPath` calls with `PathUtils.resolveFile/Folder` +- [ ] Update `readNote`, `createNote`, `updateNote`, `deleteNote`, `listNotes` +- [ ] Add path normalization to all endpoints + +#### 1.3 Enhanced Error Messages + +**File:** `error-messages.ts` (new) + +- [ ] Create error message templates with helpful guidance +- [ ] Include suggested next actions +- [ ] Add links to documentation examples +- [ ] Implement `fileNotFound()`, `folderNotFound()`, `invalidPath()` helpers + +**Example Error Format:** +``` +File not found: "path/to/file.md" + +Troubleshooting tips: +โ€ข Omit leading/trailing slashes +โ€ข Check vault-relative path casing +โ€ข Try stat("path") to verify +โ€ข Use list_notes() to see available files +``` + +#### 1.4 Testing + +- [ ] Test with Windows paths (backslashes, drive letters) +- [ ] Test with macOS paths (case-sensitive) +- [ ] Test with Linux paths +- [ ] Test trailing slash handling +- [ ] Test error message clarity + +--- + +## Phase 1.5: Enhanced Authentication & Security + +**Priority:** P0 +**Dependencies:** None +**Estimated Effort:** 2-3 days + +### Goals + +Improve bearer token authentication with secure key management, token rotation, and multiple authentication methods. + +### Tasks + +#### 1.5.1 Secure API Key Management + +**File:** `auth-utils.ts` (new) + +- [ ] Implement secure API key generation +- [ ] Add key validation and strength requirements +- [ ] Support multiple API keys with labels/names +- [ ] Add key expiration and rotation +- [ ] Store keys securely in plugin data + +**Key Requirements:** +- Minimum length: 32 characters +- Cryptographically random generation +- Optional expiration dates +- Human-readable labels for identification + +#### 1.5.2 Enhanced Authentication Middleware + +**File:** `src/server/middleware.ts` (update) + +- [ ] Add request rate limiting per API key +- [ ] Implement request logging with authentication context +- [ ] Add support for multiple authentication schemes +- [ ] Improve error messages for authentication failures +- [ ] Add authentication attempt tracking + +**Authentication Schemes:** +- Bearer token (existing, enhanced) +- API key in custom header (e.g., `X-API-Key`) +- Query parameter authentication (for testing only) + +#### 1.5.3 API Key Management UI + +**File:** `src/settings.ts` (update) + +- [ ] Add API key generation button with secure random generation +- [ ] Display list of active API keys with labels +- [ ] Add key creation/deletion interface +- [ ] Show key creation date and last used timestamp +- [ ] Add key expiration management +- [ ] Implement key visibility toggle (show/hide) +- [ ] Add "Copy to clipboard" functionality + +**UI Improvements:** +```typescript +// Settings panel additions +- "Generate New API Key" button +- Key list with: + - Label/name + - Created date + - Last used timestamp + - Expiration date (if set) + - Revoke button +- Key strength indicator +- Security best practices notice +``` + +#### 1.5.4 Authentication Audit Log + +**File:** `auth-log.ts` (new) + +- [ ] Log authentication attempts (success/failure) +- [ ] Track API key usage statistics +- [ ] Add configurable log retention +- [ ] Provide audit log export +- [ ] Display recent authentication activity in settings + +**Log Format:** +```typescript +{ + timestamp: number, + keyLabel: string, + success: boolean, + ipAddress: string, + endpoint: string, + errorReason?: string +} +``` + +#### 1.5.5 Security Enhancements + +- [ ] Add HTTPS requirement option (reject HTTP in production) +- [ ] Implement request signing for additional security +- [ ] Add IP allowlist/blocklist option +- [ ] Support for read-only API keys (restrict to read operations) +- [ ] Add permission scopes per API key + +**Permission Scopes:** +- `read` - Read operations only +- `write` - Create, update, delete operations +- `admin` - Server configuration access +- `all` - Full access (default) + +#### 1.5.6 Documentation Updates + +- [ ] Document API key generation best practices +- [ ] Add authentication examples for different clients +- [ ] Document security considerations +- [ ] Add troubleshooting guide for auth issues +- [ ] Document permission scopes and their usage + +#### 1.5.7 Testing + +- [ ] Test API key generation and validation +- [ ] Test multiple API keys with different scopes +- [ ] Test key expiration and rotation +- [ ] Test rate limiting per key +- [ ] Test authentication failure scenarios +- [ ] Test audit logging +- [ ] Security audit of authentication implementation + +--- + +## Phase 2: API Unification & Typed Results + +**Priority:** P1 +**Dependencies:** Phase 1 +**Estimated Effort:** 3-5 days + +### Goals + +Standardize parameter naming and return structured, typed results. + +### Tasks + +#### 2.1 Parameter Unification + +- [ ] Standardize on `path` parameter for all file/folder operations +- [ ] Keep `folder` as deprecated alias for backward compatibility +- [ ] Update tool schemas in `handleListTools()` +- [ ] Add deprecation warnings to documentation + +**Changes:** +- `list_notes({ folder })` โ†’ `list_notes({ path })` +- Document `folder` as deprecated but still functional + +#### 2.2 Typed Result Interfaces + +**File:** `mcp-types.ts` (update) + +Add new type definitions: + +```typescript +export type ItemKind = "file" | "directory"; + +export interface FileMetadata { + kind: "file"; + name: string; + path: string; + extension: string; + size: number; + modified: number; + created: number; +} + +export interface DirectoryMetadata { + kind: "directory"; + name: string; + path: string; + childrenCount: number; + modified: number; +} + +export interface VaultInfo { + name: string; + path: string; + totalFiles: number; + totalFolders: number; + markdownFiles: number; + totalSize: number; +} + +export interface SearchMatch { + path: string; + line: number; + column: number; + snippet: string; + matchRanges: Array<{ start: number; end: number }>; +} + +export interface SearchResult { + query: string; + matches: SearchMatch[]; + totalMatches: number; + filesSearched: number; +} +``` + +#### 2.3 Update Tool Return Values + +- [ ] Modify `listNotes` to return structured `FileMetadata[]` or `DirectoryMetadata[]` +- [ ] Modify `getVaultInfo` to return `VaultInfo` +- [ ] Modify `searchNotes` to return `SearchResult` +- [ ] Return JSON-serialized structured data instead of plain text + +#### 2.4 Documentation Updates + +- [ ] Update README with new response formats +- [ ] Add examples of structured responses +- [ ] Document backward compatibility notes + +--- + +## Phase 3: Discovery Endpoints + +**Priority:** P1 +**Dependencies:** Phase 1, Phase 2 +**Estimated Effort:** 2-3 days + +### Goals + +Add endpoints for exploring vault structure and testing path validity. + +### Tasks + +#### 3.1 Implement `stat` Tool + +- [ ] Add `stat` tool to `handleListTools()` +- [ ] Implement `stat(path)` method +- [ ] Return existence, kind, and metadata + +**Tool Schema:** +```typescript +{ + name: "stat", + description: "Get metadata for a file or folder", + inputSchema: { + type: "object", + properties: { + path: { + type: "string", + description: "Vault-relative path" + } + }, + required: ["path"] + } +} +``` + +**Returns:** `{ exists: boolean, kind?: "file" | "directory", ...metadata }` + +#### 3.2 Implement `exists` Tool + +- [ ] Add `exists` tool to `handleListTools()` +- [ ] Implement fast path validation +- [ ] Return boolean result + +**Tool Schema:** +```typescript +{ + name: "exists", + description: "Check if a file or folder exists", + inputSchema: { + type: "object", + properties: { + path: { type: "string" } + }, + required: ["path"] + } +} +``` + +**Returns:** `{ path: string, exists: boolean, kind?: "file" | "directory" }` + +#### 3.3 Testing + +- [ ] Test `stat` on files, folders, and non-existent paths +- [ ] Test `exists` with various path formats +- [ ] Verify performance of `exists` vs `stat` + +--- + +## Phase 4: Enhanced List Operations + +**Priority:** P2 +**Dependencies:** Phase 2, Phase 3 +**Estimated Effort:** 3-4 days + +### Goals + +Add powerful filtering, recursion control, and pagination to list operations. + +### Tasks + +#### 4.1 Enhanced `list` Tool + +Replace `list_notes` with more powerful `list` tool. + +**Tool Schema:** +```typescript +{ + name: "list", + description: "List files and/or directories with filtering", + inputSchema: { + type: "object", + properties: { + path: { type: "string" }, + recursive: { type: "boolean", default: false }, + includes: { type: "array", items: { type: "string" } }, + excludes: { type: "array", items: { type: "string" } }, + only: { + type: "string", + enum: ["files", "directories", "any"], + default: "any" + }, + limit: { type: "number" }, + cursor: { type: "string" }, + withFrontmatterSummary: { type: "boolean", default: false } + } + } +} +``` + +**Note:** When `withFrontmatterSummary` is true, include parsed frontmatter keys (title, tags) in metadata without fetching full content. + +#### 4.2 Implement Glob Matching + +**File:** `glob-utils.ts` (new) + +- [ ] Implement or import glob matching library (e.g., minimatch) +- [ ] Support `*`, `**`, `?` wildcards +- [ ] Handle include/exclude patterns + +#### 4.3 Implement Pagination + +- [ ] Add cursor-based pagination +- [ ] Encode cursor with last item path +- [ ] Return `nextCursor` in results + +**Result Format:** +```typescript +{ + items: Array, + totalCount: number, + hasMore: boolean, + nextCursor?: string +} +``` + +#### 4.4 Backward Compatibility + +- [ ] Keep `list_notes` as alias to `list` with appropriate defaults +- [ ] Add deprecation notice in documentation + +#### 4.5 Frontmatter Summary Option + +- [ ] Add `withFrontmatterSummary` parameter to list tool +- [ ] Extract frontmatter keys (title, tags, aliases) without reading full content +- [ ] Include in `FileMetadata` as optional `frontmatterSummary` field +- [ ] Optimize to avoid full file reads when possible + +#### 4.6 Testing + +- [ ] Test recursive vs non-recursive listing +- [ ] Test glob include/exclude patterns +- [ ] Test pagination with various limits +- [ ] Test filtering by type (files/directories/any) +- [ ] Test frontmatter summary extraction +- [ ] Performance test with large vaults (10k+ files) + +--- + +## Phase 5: Advanced Read Operations + +**Priority:** P3 +**Dependencies:** Phase 2 +**Estimated Effort:** 2-3 days + +### Goals + +Add options for reading notes with frontmatter parsing and specialized file type support. + +### Tasks + +#### 5.1 Enhanced `read_note` Tool + +**Updated Schema:** +```typescript +{ + name: "read_note", + description: "Read note with optional frontmatter parsing", + inputSchema: { + type: "object", + properties: { + path: { type: "string" }, + withFrontmatter: { type: "boolean", default: true }, + withContent: { type: "boolean", default: true }, + parseFrontmatter: { type: "boolean", default: false } + }, + required: ["path"] + } +} +``` + +#### 5.2 Frontmatter Parsing + +**File:** `frontmatter-utils.ts` (new) + +- [ ] Implement frontmatter extraction +- [ ] Parse YAML frontmatter +- [ ] Separate frontmatter from content +- [ ] Return structured `ParsedNote` object + +#### 5.3 Excalidraw Support + +**Tool:** `read_excalidraw` + +- [ ] Add specialized tool for Excalidraw files +- [ ] Extract plugin metadata +- [ ] Return element counts +- [ ] Provide safe preview summary +- [ ] Optional compressed data inclusion + +**Schema:** +```typescript +{ + name: "read_excalidraw", + description: "Read Excalidraw drawing with metadata", + inputSchema: { + type: "object", + properties: { + path: { type: "string" }, + includeCompressed: { type: "boolean", default: false }, + includePreview: { type: "boolean", default: true } + }, + required: ["path"] + } +} +``` + +#### 5.4 Testing + +- [ ] Test frontmatter parsing with various YAML formats +- [ ] Test with notes that have no frontmatter +- [ ] Test Excalidraw file reading +- [ ] Test parameter combinations + +--- + +## Phase 6: Powerful Search + +**Priority:** P2 +**Dependencies:** Phase 2 +**Estimated Effort:** 4-5 days + +### Goals + +Implement regex search, snippet extraction, and specialized search helpers. + +### Tasks + +#### 6.1 Enhanced `search` Tool + +**Tool Schema:** +```typescript +{ + name: "search", + description: "Search vault with advanced filtering", + inputSchema: { + type: "object", + properties: { + query: { type: "string" }, + isRegex: { type: "boolean", default: false }, + caseSensitive: { type: "boolean", default: false }, + includes: { type: "array", items: { type: "string" } }, + excludes: { type: "array", items: { type: "string" } }, + folder: { type: "string" }, + returnSnippets: { type: "boolean", default: true }, + snippetLength: { type: "number", default: 100 }, + maxResults: { type: "number", default: 100 } + }, + required: ["query"] + } +} +``` + +#### 6.2 Search Implementation + +**File:** `search-utils.ts` (new) + +- [ ] Implement regex and literal search +- [ ] Extract surrounding context snippets +- [ ] Calculate match ranges for highlighting +- [ ] Support glob filtering +- [ ] Limit results and track statistics + +**Result Format:** +```typescript +{ + query: string, + isRegex: boolean, + matches: SearchMatch[], + totalMatches: number, + filesSearched: number, + filesWithMatches: number +} +``` + +#### 6.3 Waypoint Search Shorthand + +**Tool:** `search_waypoints` + +- [ ] Add specialized tool for finding Waypoint markers +- [ ] Search for `%% Begin Waypoint %%` ... `%% End Waypoint %%` +- [ ] Return locations and parsed content + +**Schema:** +```typescript +{ + name: "search_waypoints", + description: "Find all Waypoint markers in vault", + inputSchema: { + type: "object", + properties: { + folder: { type: "string" } + } + } +} +``` + +#### 6.4 Testing + +- [ ] Test literal vs regex search +- [ ] Test case sensitivity +- [ ] Test snippet extraction +- [ ] Test glob filtering +- [ ] Test waypoint search +- [ ] Performance test with large files + +--- + +## Phase 7: Waypoint Support + +**Priority:** P3 +**Dependencies:** Phase 6 +**Estimated Effort:** 3-4 days + +### Goals + +Add specialized tools for working with Waypoint plugin markers. + +### Tasks + +#### 7.1 Implement `get_folder_waypoint` Tool + +**Tool Schema:** +```typescript +{ + name: "get_folder_waypoint", + description: "Get Waypoint block from a folder note", + inputSchema: { + type: "object", + properties: { + path: { type: "string" } + }, + required: ["path"] + } +} +``` + +**Implementation:** +- [ ] Find `%% Begin Waypoint %%` ... `%% End Waypoint %%` block +- [ ] Extract fenced block range (line numbers) +- [ ] Parse links within the block +- [ ] Return structured data + +**Result Format:** +```typescript +{ + path: string, + hasWaypoint: boolean, + waypointRange?: { start: number, end: number }, + links?: string[], + rawContent?: string +} +``` + +#### 7.2 Waypoint Edit Protection + +- [ ] Add validation to `update_note` and `update_sections` tools +- [ ] Refuse edits that would affect `%% Begin Waypoint %%` ... `%% End Waypoint %%` blocks +- [ ] Return clear error message when waypoint edit is attempted +- [ ] Provide option to force edit with explicit `allowWaypointEdit: true` flag + +#### 7.3 Implement `is_folder_note` Tool + +**Tool Schema:** +```typescript +{ + name: "is_folder_note", + description: "Check if a note is a folder note", + inputSchema: { + type: "object", + properties: { + path: { type: "string" } + }, + required: ["path"] + } +} +``` + +**Implementation:** +- [ ] Check if basename equals folder name +- [ ] Check for Waypoint markers +- [ ] Return boolean and metadata + +**Result Format:** +```typescript +{ + path: string, + isFolderNote: boolean, + reason: "basename_match" | "waypoint_marker" | "both" | "none", + folderPath?: string +} +``` + +#### 7.4 Testing + +- [ ] Test with various Waypoint formats +- [ ] Test folder note detection +- [ ] Test with nested folders +- [ ] Test edge cases (empty waypoints, malformed markers) + +--- + +## Phase 8: Write Operations & Concurrency + +**Priority:** P1 +**Dependencies:** Phase 1, Phase 2 +**Estimated Effort:** 5-6 days + +### Goals + +Implement safe write operations with concurrency control, partial updates, conflict resolution, and file rename/move with automatic link updates. + +### Tasks + +#### 8.1 Partial Update Tools + +**Tool:** `update_frontmatter` + +- [ ] Add tool for updating only frontmatter without touching content +- [ ] Support patch operations (add, update, remove keys) +- [ ] Preserve content and formatting + +**Schema:** +```typescript +{ + name: "update_frontmatter", + description: "Update frontmatter fields without modifying content", + inputSchema: { + type: "object", + properties: { + path: { type: "string" }, + patch: { + type: "object", + description: "Frontmatter fields to add/update" + }, + remove: { + type: "array", + items: { type: "string" }, + description: "Frontmatter keys to remove" + }, + ifMatch: { type: "string", description: "ETag for concurrency control" } + }, + required: ["path", "patch"] + } +} +``` + +**Tool:** `update_sections` + +- [ ] Add tool for updating specific sections of a note +- [ ] Support line-based or heading-based edits +- [ ] Reduce race conditions by avoiding full overwrites + +**Schema:** +```typescript +{ + name: "update_sections", + description: "Update specific sections of a note", + inputSchema: { + type: "object", + properties: { + path: { type: "string" }, + edits: { + type: "array", + items: { + type: "object", + properties: { + startLine: { type: "number" }, + endLine: { type: "number" }, + content: { type: "string" } + } + } + }, + ifMatch: { type: "string" } + }, + required: ["path", "edits"] + } +} +``` + +#### 8.2 Concurrency Control + +**File:** `version-utils.ts` (new) + +- [ ] Implement ETag/versionId generation based on file mtime and size +- [ ] Add `versionId` to all read responses +- [ ] Validate `ifMatch` parameter on write operations +- [ ] Return new `versionId` on successful writes +- [ ] Return 412 Precondition Failed on version mismatch + +**Updated Read Response:** +```typescript +{ + path: string, + content: string, + versionId: string, // e.g., "mtime-size" hash + modified: number +} +``` + +#### 8.3 Enhanced Create with Conflict Strategy + +- [ ] Update `create_note` tool with `onConflict` parameter +- [ ] Support strategies: `"error"` (default), `"overwrite"`, `"rename"` +- [ ] Auto-create parent directories or return actionable error +- [ ] Return created path (may differ if renamed) + +**Updated Schema:** +```typescript +{ + name: "create_note", + description: "Create a new note with conflict handling", + inputSchema: { + type: "object", + properties: { + path: { type: "string" }, + content: { type: "string" }, + onConflict: { + type: "string", + enum: ["error", "overwrite", "rename"], + default: "error" + }, + createParents: { type: "boolean", default: true } + }, + required: ["path", "content"] + } +} +``` + +#### 8.4 Timestamp Handling + +- [ ] Add `preserveTimestamps` option to write operations +- [ ] Add `autoTimestamp` option to update frontmatter with `updated` field +- [ ] Document Obsidian's automatic timestamp behavior +- [ ] Allow clients to control timestamp strategy + +**Options:** +```typescript +{ + preserveTimestamps?: boolean, // Don't modify file mtime + autoTimestamp?: boolean, // Update frontmatter 'updated' field + timestampField?: string // Custom field name (default: 'updated') +} +``` + +#### 8.5 Rename/Move File + +**Tool:** `rename_file` (or `move_file`) + +- [ ] Add tool for renaming or moving files using Obsidian's FileManager +- [ ] Use `app.fileManager.renameFile()` to maintain link integrity +- [ ] Automatically update all wikilinks that reference the file +- [ ] Support moving to different folders +- [ ] Handle conflicts with existing files + +**Schema:** +```typescript +{ + name: "rename_file", + description: "Rename or move a file, automatically updating all links", + inputSchema: { + type: "object", + properties: { + path: { type: "string", description: "Current file path" }, + newPath: { type: "string", description: "New file path (can be in different folder)" }, + updateLinks: { type: "boolean", default: true, description: "Update wikilinks automatically" }, + ifMatch: { type: "string", description: "ETag for concurrency control" } + }, + required: ["path", "newPath"] + } +} +``` + +**Response:** +```typescript +{ + success: boolean, + oldPath: string, + newPath: string, + linksUpdated: number, // Count of files with updated links + affectedFiles: string[] // Paths of files that had links updated +} +``` + +**Implementation Notes:** +- Use `app.fileManager.renameFile(file, newPath)` from [Obsidian API](https://docs.obsidian.md/Reference/TypeScript+API/FileManager/renameFile) +- This automatically updates all wikilinks in the vault +- Handles both rename (same folder) and move (different folder) operations +- Preserves file content and metadata + +#### 8.6 Safe Delete + +- [ ] Update `delete_note` tool with soft delete option +- [ ] Move to `.trash/` folder instead of permanent deletion +- [ ] Add `dryRun` option to preview deletion +- [ ] Return destination path for soft deletes + +**Updated Schema:** +```typescript +{ + name: "delete_note", + description: "Delete a note with safety options", + inputSchema: { + type: "object", + properties: { + path: { type: "string" }, + soft: { type: "boolean", default: true }, + dryRun: { type: "boolean", default: false }, + ifMatch: { type: "string" } + }, + required: ["path"] + } +} +``` + +**Response:** +```typescript +{ + deleted: boolean, + path: string, + destination?: string, // For soft deletes + dryRun: boolean +} +``` + +#### 8.7 Testing + +- [ ] Test concurrent updates with version control +- [ ] Test partial frontmatter updates +- [ ] Test section updates +- [ ] Test conflict strategies (error, overwrite, rename) +- [ ] Test rename/move operations with link updates +- [ ] Test moving files between folders +- [ ] Test rename conflicts with existing files +- [ ] Verify automatic wikilink updates after rename +- [ ] Test soft delete and trash functionality +- [ ] Test parent directory creation +- [ ] Test timestamp preservation + +--- + +## Phase 9: Linking & Backlinks + +**Priority:** P2 +**Dependencies:** Phase 2 +**Estimated Effort:** 3-4 days + +### Goals + +Add tools for working with wikilinks, resolving links, and querying backlinks. + +### Tasks + +#### 9.1 Wikilink Validation + +**Tool:** `validate_wikilinks` + +- [ ] Add tool to validate all wikilinks in a note +- [ ] Report unresolved `[[links]]` +- [ ] Suggest potential targets for broken links +- [ ] Support both `[[link]]` and `[[link|alias]]` formats + +**Schema:** +```typescript +{ + name: "validate_wikilinks", + description: "Validate wikilinks in a note and report unresolved links", + inputSchema: { + type: "object", + properties: { + path: { type: "string" } + }, + required: ["path"] + } +} +``` + +**Response:** +```typescript +{ + path: string, + totalLinks: number, + resolvedLinks: Array<{ + text: string, + target: string, + alias?: string + }>, + unresolvedLinks: Array<{ + text: string, + line: number, + suggestions: string[] // Potential matches + }> +} +``` + +#### 9.2 Link Resolution + +**Tool:** `resolve_wikilink` + +- [ ] Add tool to resolve a wikilink from a source note +- [ ] Handle relative paths and aliases +- [ ] Return target path if resolvable +- [ ] Support Obsidian's link resolution rules + +**Schema:** +```typescript +{ + name: "resolve_wikilink", + description: "Resolve a wikilink to its target path", + inputSchema: { + type: "object", + properties: { + sourcePath: { type: "string" }, + linkText: { type: "string" } + }, + required: ["sourcePath", "linkText"] + } +} +``` + +**Response:** +```typescript +{ + sourcePath: string, + linkText: string, + resolved: boolean, + targetPath?: string, + suggestions?: string[] // If not resolved +} +``` + +#### 9.3 Backlinks API + +**Tool:** `backlinks` + +- [ ] Add tool to query backlinks for a note +- [ ] Return all notes that link to the target +- [ ] Support `includeUnlinked` for unlinked mentions +- [ ] Include context snippets for each backlink + +**Schema:** +```typescript +{ + name: "backlinks", + description: "Get backlinks to a note", + inputSchema: { + type: "object", + properties: { + path: { type: "string" }, + includeUnlinked: { type: "boolean", default: false }, + includeSnippets: { type: "boolean", default: true } + }, + required: ["path"] + } +} +``` + +**Response:** +```typescript +{ + path: string, + backlinks: Array<{ + sourcePath: string, + type: "linked" | "unlinked", + occurrences: Array<{ + line: number, + snippet: string + }> + }>, + totalBacklinks: number +} +``` + +#### 9.4 Implementation Details + +**File:** `link-utils.ts` (new) + +- [ ] Implement wikilink parsing (regex for `[[...]]`) +- [ ] Implement link resolution using Obsidian's MetadataCache +- [ ] Build backlink index from MetadataCache +- [ ] Handle edge cases (circular links, missing files) + +#### 9.5 Testing + +- [ ] Test wikilink validation with various formats +- [ ] Test link resolution with aliases +- [ ] Test backlinks with linked and unlinked mentions +- [ ] Test with nested folders and relative paths +- [ ] Test performance with large vaults + +--- + +## Testing & Documentation + +### Unit Tests + +**File:** `tests/` (new directory) + +- [ ] Set up Jest or similar testing framework +- [ ] Write unit tests for `PathUtils` +- [ ] Write unit tests for `GlobUtils` +- [ ] Write unit tests for `FrontmatterUtils` +- [ ] Write unit tests for `SearchUtils` +- [ ] Achieve >80% code coverage + +### Integration Tests + +- [ ] Test full MCP request/response cycle +- [ ] Test authentication and CORS +- [ ] Test error handling +- [ ] Test with real vault data + +### Documentation + +**Files to Update:** +- [ ] `README.md` - Update with new tools and examples +- [ ] `API.md` (new) - Comprehensive API reference +- [ ] `EXAMPLES.md` (new) - Usage examples for each tool +- [ ] `COOKBOOK.md` (new) - Quick-start recipes for common tasks +- [ ] `TROUBLESHOOTING.md` (update) - Expand with new scenarios +- [ ] `MIGRATION.md` (new) - Guide for upgrading from v1.0 + +**Documentation Sections:** +- [ ] Tool reference with schemas +- [ ] Response format examples +- [ ] Error handling guide +- [ ] Platform-specific notes (Windows/macOS/Linux) +- [ ] Performance characteristics +- [ ] Backward compatibility notes +- [ ] Concurrency and version control guide +- [ ] Link resolution and backlinks guide + +**Quick-Start Cookbook:** +- [ ] List all notes in a folder +- [ ] Create a note with frontmatter +- [ ] Read and parse frontmatter +- [ ] Update only frontmatter fields +- [ ] Search with regex and filters +- [ ] Delete with soft delete +- [ ] Validate and resolve wikilinks +- [ ] Query backlinks +- [ ] Work with Waypoint folder notes + +**Troubleshooting Table:** +- [ ] Trailing slash issues +- [ ] Case sensitivity differences (Windows vs macOS/Linux) +- [ ] Missing parent directories +- [ ] Concurrency failures (version mismatch) +- [ ] Broken wikilinks +- [ ] Waypoint edit protection + +### Performance Documentation + +**File:** `PERFORMANCE.md` (new) + +- [ ] Document limits (max results, recursion depth) +- [ ] Provide performance benchmarks +- [ ] Recommend best practices for large vaults +- [ ] Document pagination strategies + +--- + +## Performance Considerations + +### Optimization Targets + +- [ ] **List operations**: Cache folder structure, implement lazy loading +- [ ] **Search operations**: Consider indexing for large vaults (>10k files) +- [ ] **Recursive operations**: Implement depth limits and timeout protection +- [ ] **Memory usage**: Stream large files, limit in-memory buffers + +### Benchmarks to Track + +- [ ] List 10k files (recursive) +- [ ] Search across 10k files +- [ ] Read 100 notes sequentially +- [ ] Parse 1000 frontmatter blocks + +### Performance Limits + +Document and enforce: +- Max recursion depth: 50 levels +- Max search results: 10,000 matches +- Max file size for search: 10 MB +- Request timeout: 30 seconds + +--- + +## Implementation Order + +### Sprint 1 (Week 1-2): Foundation +1. Phase 1: Path Normalization & Error Handling +2. Phase 1.5: Enhanced Authentication & Security +3. Phase 2: API Unification & Typed Results +4. Phase 3: Discovery Endpoints + +### Sprint 2 (Week 3-4): Core Operations +5. Phase 8: Write Operations & Concurrency +6. Phase 4: Enhanced List Operations + +### Sprint 3 (Week 5-6): Advanced Read & Search +7. Phase 5: Advanced Read Operations +8. Phase 6: Powerful Search + +### Sprint 4 (Week 7-8): Specialized Features +9. Phase 7: Waypoint Support +10. Phase 9: Linking & Backlinks + +### Sprint 5 (Week 9-10): Polish & Release +11. Testing & Documentation +12. Performance Optimization +13. Quick-start Cookbook & Examples +14. Release Preparation + +--- + +## Success Criteria + +### Functional Requirements +- [ ] All path formats work consistently across platforms +- [ ] Error messages are clear and actionable +- [ ] All tools return structured, typed data +- [ ] Search supports regex and glob filtering +- [ ] List operations support pagination and frontmatter summaries +- [ ] Write operations support concurrency control +- [ ] Partial updates (frontmatter, sections) work correctly +- [ ] Conflict resolution strategies work as expected +- [ ] Rename/move operations update wikilinks automatically +- [ ] Wikilink validation and resolution work correctly +- [ ] Backlinks API returns accurate results +- [ ] Waypoint tools work with common patterns and protect edits + +### Non-Functional Requirements +- [ ] >80% test coverage +- [ ] All tools documented with examples +- [ ] Performance benchmarks established +- [ ] Backward compatibility maintained +- [ ] No breaking changes to existing tools + +### User Experience +- [ ] Error messages include troubleshooting tips +- [ ] API is consistent and predictable +- [ ] Documentation is comprehensive +- [ ] Migration guide is clear + +--- + +## Future Considerations (Post-Roadmap) + +### Potential Future Features +- **Versioned API**: Introduce v1 stable contract for incremental, non-breaking improvements +- **Resources API**: Expose notes as MCP resources +- **Prompts API**: Provide templated prompts for common operations +- **Batch Operations**: Support multiple operations in single request +- **Webhooks**: Notify clients of vault changes +- **Graph API**: Enhanced graph visualization and traversal +- **Tag API**: Query and manipulate tags +- **Canvas API**: Read and manipulate canvas files +- **Dataview Integration**: Query vault using Dataview syntax +- **Template System**: Apply templates with variable substitution +- **Merge Conflicts**: Three-way merge for concurrent edits + +### Performance Enhancements +- **Indexing**: Build search index for large vaults +- **Caching**: Cache frequently accessed data +- **Streaming**: Stream large result sets +- **Compression**: Compress large responses + +--- + +## Notes + +- Maintain backward compatibility throughout all phases +- Deprecate old APIs gracefully with clear migration paths +- Prioritize user feedback and real-world usage patterns +- Keep security as a top priority (localhost-only, authentication) +- Document performance characteristics for all operations +- Consider mobile support in future (currently desktop-only) + +--- + +**End of Roadmap** diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md new file mode 100644 index 0000000..e090626 --- /dev/null +++ b/TROUBLESHOOTING.md @@ -0,0 +1,308 @@ +# Troubleshooting Guide + +## Plugin Won't Load + +### Check Required Files + +Ensure these files exist in the plugin directory: +```bash +ls -la /path/to/vault/.obsidian/plugins/obsidian-mcp-server/ +``` + +Required files: +- โœ… `main.js` (should be ~846KB) +- โœ… `manifest.json` +- โœ… `styles.css` + +### Check Obsidian Console + +1. Open Obsidian +2. Press `Ctrl+Shift+I` (Windows/Linux) or `Cmd+Option+I` (Mac) +3. Go to the **Console** tab +4. Look for errors related to `obsidian-mcp-server` + +Common errors: +- **Module not found**: Rebuild the plugin with `npm run build` +- **Syntax error**: Check the build completed successfully +- **Permission error**: Ensure files are readable + +### Verify Plugin is Enabled + +1. Go to **Settings** โ†’ **Community Plugins** +2. Find **MCP Server** in the list +3. Ensure the toggle is **ON** +4. If not visible, click **Reload** or restart Obsidian + +### Check Manifest + +Verify `manifest.json` contains: +```json +{ + "id": "obsidian-mcp-server", + "name": "MCP Server", + "version": "1.0.0", + "minAppVersion": "0.15.0", + "description": "Exposes Obsidian vault operations via Model Context Protocol (MCP) over HTTP", + "author": "", + "authorUrl": "", + "isDesktopOnly": true +} +``` + +### Rebuild from Source + +If the plugin still won't load: + +```bash +cd /path/to/vault/.obsidian/plugins/obsidian-mcp-server +npm install +npm run build +``` + +Then restart Obsidian. + +### Check Obsidian Version + +This plugin requires: +- **Minimum Obsidian version**: 0.15.0 +- **Desktop only** (not mobile) + +Check your version: +1. **Settings** โ†’ **About** +2. Look for "Current version" + +### Verify Node.js Built-ins + +The plugin uses Node.js modules (http, express). Ensure you're running on desktop Obsidian, not mobile. + +## Plugin Loads But Shows No Info + +### Check Plugin Description + +If the plugin appears in the list but shows no description: + +1. Check `manifest.json` has a `description` field +2. Restart Obsidian +3. Try disabling and re-enabling the plugin + +### Check for Errors on Load + +1. Open Console (`Ctrl+Shift+I`) +2. Disable the plugin +3. Re-enable it +4. Watch for errors in console + +## Server Won't Start + +### Port Already in Use + +**Error**: "Port 3000 is already in use" + +**Solution**: +1. Go to **Settings** โ†’ **MCP Server** +2. Change port to something else (e.g., 3001, 3002) +3. Try starting again + +Or find and kill the process using port 3000: +```bash +# Linux/Mac +lsof -i :3000 +kill -9 + +# Windows +netstat -ano | findstr :3000 +taskkill /PID /F +``` + +### Module Not Found + +**Error**: "Cannot find module 'express'" or similar + +**Solution**: +```bash +cd /path/to/vault/.obsidian/plugins/obsidian-mcp-server +npm install +npm run build +``` + +Restart Obsidian. + +### Permission Denied + +**Error**: "EACCES" or "Permission denied" + +**Solution**: +- Try a different port (above 1024) +- Check firewall settings +- Run Obsidian with appropriate permissions + +## Server Starts But Can't Connect + +### Check Server is Running + +Look at the status bar (bottom of Obsidian): +- Should show: `MCP: Running (3000)` +- If shows: `MCP: Stopped` - server isn't running + +### Test Health Endpoint + +Open browser or use curl: +```bash +curl http://127.0.0.1:3000/health +``` + +Should return: +```json +{"status":"ok","timestamp":1234567890} +``` + +### Check Localhost Binding + +The server only binds to `127.0.0.1` (localhost). You cannot connect from: +- Other computers on the network +- External IP addresses +- Public internet + +This is by design for security. + +### Test MCP Endpoint + +```bash +curl -X POST http://127.0.0.1:3000/mcp \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"ping"}' +``` + +Should return: +```json +{"jsonrpc":"2.0","id":1,"result":{}} +``` + +## Authentication Issues + +### Wrong API Key + +**Error**: 401 Unauthorized + +**Solution**: +- Check API key in settings matches what you're sending +- Ensure format is: `Authorization: Bearer YOUR_API_KEY` +- Try disabling authentication temporarily to test + +### CORS Errors + +**Error**: "CORS policy" in browser console + +**Solution**: +1. Go to **Settings** โ†’ **MCP Server** +2. Ensure "Enable CORS" is **ON** +3. Check "Allowed Origins" includes your origin or `*` +4. Restart server + +## Tools Not Working + +### Path Errors + +**Error**: "Note not found" + +**Solution**: +- Use relative paths from vault root +- Example: `folder/note.md` not `/full/path/to/note.md` +- Don't include vault name in path + +### Permission Errors + +**Error**: "EACCES" or "Permission denied" + +**Solution**: +- Check file permissions in vault +- Ensure Obsidian has file system access +- Check vault is not read-only + +### Search Returns Nothing + +**Issue**: `search_notes` returns no results + +**Solution**: +- Check query is not empty +- Search is case-insensitive +- Searches both filename and content +- Try simpler query + +## Getting Help + +### Collect Debug Information + +When reporting issues, include: + +1. **Obsidian version**: Settings โ†’ About +2. **Plugin version**: Check manifest.json +3. **Operating System**: Windows/Mac/Linux +4. **Error messages**: From console (Ctrl+Shift+I) +5. **Steps to reproduce**: What you did before the error + +### Console Logs + +Enable detailed logging: +1. Open Console (`Ctrl+Shift+I`) +2. Try the failing operation +3. Copy all red error messages +4. Include in your report + +### Test Client Output + +Run the test client and include output: +```bash +node test-client.js +``` + +### Check GitHub Issues + +Before creating a new issue: +1. Search existing issues +2. Check if it's already reported +3. See if there's a workaround + +## Common Solutions + +### "Have you tried turning it off and on again?" + +Seriously, this fixes many issues: +1. Stop the server +2. Disable the plugin +3. Restart Obsidian +4. Enable the plugin +5. Start the server + +### Clean Reinstall + +If all else fails: +```bash +# Backup settings first! +cd /path/to/vault/.obsidian/plugins +rm -rf obsidian-mcp-server +# Re-install plugin +cd obsidian-mcp-server +npm install +npm run build +``` + +Restart Obsidian. + +### Reset Settings + +If settings are corrupted: +1. Stop server +2. Disable plugin +3. Delete `/path/to/vault/.obsidian/plugins/obsidian-mcp-server/data.json` +4. Re-enable plugin +5. Reconfigure settings + +## Still Having Issues? + +1. Check the README.md for documentation +2. Review QUICKSTART.md for setup steps +3. Run the test client to verify server +4. Check Obsidian console for errors +5. Try a clean rebuild +6. Create a GitHub issue with debug info diff --git a/esbuild.config.mjs b/esbuild.config.mjs new file mode 100644 index 0000000..291768b --- /dev/null +++ b/esbuild.config.mjs @@ -0,0 +1,51 @@ +import esbuild from "esbuild"; +import process from "process"; +import builtins from "builtin-modules"; + +const banner = +`/* +THIS IS A GENERATED/BUNDLED FILE BY ESBUILD +if you want to view the source, please visit the github repository of this plugin +*/ +`; + +const prod = (process.argv[2] === "production"); + +const context = await esbuild.context({ + banner: { + js: banner, + }, + entryPoints: ["src/main.ts"], + bundle: true, + external: [ + "obsidian", + "electron", + "@codemirror/autocomplete", + "@codemirror/collab", + "@codemirror/commands", + "@codemirror/language", + "@codemirror/lint", + "@codemirror/search", + "@codemirror/state", + "@codemirror/view", + "@lezer/common", + "@lezer/highlight", + "@lezer/lr", + ...builtins + ], + format: "cjs", + target: "es2018", + platform: "node", + logLevel: "info", + sourcemap: prod ? false : "inline", + treeShaking: true, + outfile: "main.js", + minify: prod, +}); + +if (prod) { + await context.rebuild(); + process.exit(0); +} else { + await context.watch(); +} diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..84d61b1 --- /dev/null +++ b/manifest.json @@ -0,0 +1,8 @@ +{ + "id": "obsidian-mcp-server", + "name": "MCP Server", + "version": "1.0.0", + "minAppVersion": "0.15.0", + "description": "Exposes Obsidian vault operations via Model Context Protocol (MCP) over HTTP", + "isDesktopOnly": true +} diff --git a/old-structure/main.ts b/old-structure/main.ts new file mode 100644 index 0000000..98c8133 --- /dev/null +++ b/old-structure/main.ts @@ -0,0 +1,275 @@ +import { App, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian'; +import { MCPServer, MCPServerSettings } from './mcp-server'; + +interface MCPPluginSettings extends MCPServerSettings { + autoStart: boolean; +} + +const DEFAULT_SETTINGS: MCPPluginSettings = { + port: 3000, + enableCORS: true, + allowedOrigins: ['*'], + apiKey: '', + enableAuth: false, + autoStart: false +} + +export default class MCPServerPlugin extends Plugin { + settings: MCPPluginSettings; + mcpServer: MCPServer | null = null; + statusBarItem: HTMLElement | null = null; + + async onload() { + await this.loadSettings(); + + // Add status bar item + this.statusBarItem = this.addStatusBarItem(); + this.updateStatusBar(); + + // Add ribbon icon to toggle server + this.addRibbonIcon('server', 'Toggle MCP Server', async () => { + if (this.mcpServer?.isRunning()) { + await this.stopServer(); + } else { + await this.startServer(); + } + }); + + // Add commands + this.addCommand({ + id: 'start-mcp-server', + name: 'Start MCP Server', + callback: async () => { + await this.startServer(); + } + }); + + this.addCommand({ + id: 'stop-mcp-server', + name: 'Stop MCP Server', + callback: async () => { + await this.stopServer(); + } + }); + + this.addCommand({ + id: 'restart-mcp-server', + name: 'Restart MCP Server', + callback: async () => { + await this.stopServer(); + await this.startServer(); + } + }); + + // Add settings tab + this.addSettingTab(new MCPServerSettingTab(this.app, this)); + + // Auto-start if enabled + if (this.settings.autoStart) { + await this.startServer(); + } + } + + async onunload() { + await this.stopServer(); + } + + async startServer() { + if (this.mcpServer?.isRunning()) { + new Notice('MCP Server is already running'); + return; + } + + try { + this.mcpServer = new MCPServer(this.app, this.settings); + await this.mcpServer.start(); + new Notice(`MCP Server started on port ${this.settings.port}`); + this.updateStatusBar(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + new Notice(`Failed to start MCP Server: ${message}`); + console.error('MCP Server start error:', error); + } + } + + async stopServer() { + if (!this.mcpServer?.isRunning()) { + new Notice('MCP Server is not running'); + return; + } + + try { + await this.mcpServer.stop(); + new Notice('MCP Server stopped'); + this.updateStatusBar(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + new Notice(`Failed to stop MCP Server: ${message}`); + console.error('MCP Server stop error:', error); + } + } + + updateStatusBar() { + if (this.statusBarItem) { + const isRunning = this.mcpServer?.isRunning() ?? false; + this.statusBarItem.setText( + isRunning + ? `MCP: Running (${this.settings.port})` + : 'MCP: Stopped' + ); + this.statusBarItem.addClass('mcp-status-bar'); + } + } + + async loadSettings() { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + } + + async saveSettings() { + await this.saveData(this.settings); + // Update server settings if it's running + if (this.mcpServer) { + this.mcpServer.updateSettings(this.settings); + } + } +} + +class MCPServerSettingTab extends PluginSettingTab { + plugin: MCPServerPlugin; + + constructor(app: App, plugin: MCPServerPlugin) { + super(app, plugin); + this.plugin = plugin; + } + + display(): void { + const {containerEl} = this; + + containerEl.empty(); + + containerEl.createEl('h2', {text: 'MCP Server Settings'}); + + // Auto-start setting + new Setting(containerEl) + .setName('Auto-start server') + .setDesc('Automatically start the MCP server when Obsidian launches') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.autoStart) + .onChange(async (value) => { + this.plugin.settings.autoStart = value; + await this.plugin.saveSettings(); + })); + + // Port setting + new Setting(containerEl) + .setName('Port') + .setDesc('Port number for the HTTP server (requires restart)') + .addText(text => text + .setPlaceholder('3000') + .setValue(String(this.plugin.settings.port)) + .onChange(async (value) => { + const port = parseInt(value); + if (!isNaN(port) && port > 0 && port < 65536) { + this.plugin.settings.port = port; + await this.plugin.saveSettings(); + } + })); + + // CORS setting + new Setting(containerEl) + .setName('Enable CORS') + .setDesc('Enable Cross-Origin Resource Sharing') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.enableCORS) + .onChange(async (value) => { + this.plugin.settings.enableCORS = value; + await this.plugin.saveSettings(); + })); + + // Allowed origins + new Setting(containerEl) + .setName('Allowed origins') + .setDesc('Comma-separated list of allowed origins (* for all)') + .addText(text => text + .setPlaceholder('*') + .setValue(this.plugin.settings.allowedOrigins.join(', ')) + .onChange(async (value) => { + this.plugin.settings.allowedOrigins = value + .split(',') + .map(s => s.trim()) + .filter(s => s.length > 0); + await this.plugin.saveSettings(); + })); + + // Authentication + new Setting(containerEl) + .setName('Enable authentication') + .setDesc('Require API key for requests') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.enableAuth) + .onChange(async (value) => { + this.plugin.settings.enableAuth = value; + await this.plugin.saveSettings(); + })); + + // API Key + new Setting(containerEl) + .setName('API Key') + .setDesc('API key for authentication (Bearer token)') + .addText(text => text + .setPlaceholder('Enter API key') + .setValue(this.plugin.settings.apiKey || '') + .onChange(async (value) => { + this.plugin.settings.apiKey = value; + await this.plugin.saveSettings(); + })); + + // Server status + containerEl.createEl('h3', {text: 'Server Status'}); + + const statusEl = containerEl.createEl('div', {cls: 'mcp-server-status'}); + const isRunning = this.plugin.mcpServer?.isRunning() ?? false; + + statusEl.createEl('p', { + text: isRunning + ? `โœ… Server is running on http://127.0.0.1:${this.plugin.settings.port}/mcp` + : 'โญ• Server is stopped' + }); + + // Control buttons + const buttonContainer = containerEl.createEl('div', {cls: 'mcp-button-container'}); + + if (isRunning) { + buttonContainer.createEl('button', {text: 'Stop Server'}) + .addEventListener('click', async () => { + await this.plugin.stopServer(); + this.display(); // Refresh display + }); + + buttonContainer.createEl('button', {text: 'Restart Server'}) + .addEventListener('click', async () => { + await this.plugin.stopServer(); + await this.plugin.startServer(); + this.display(); // Refresh display + }); + } else { + buttonContainer.createEl('button', {text: 'Start Server'}) + .addEventListener('click', async () => { + await this.plugin.startServer(); + this.display(); // Refresh display + }); + } + + // Connection info + if (isRunning) { + containerEl.createEl('h3', {text: 'Connection Information'}); + + const infoEl = containerEl.createEl('div', {cls: 'mcp-connection-info'}); + infoEl.createEl('p', {text: 'MCP Endpoint:'}); + infoEl.createEl('code', {text: `http://127.0.0.1:${this.plugin.settings.port}/mcp`}); + + infoEl.createEl('p', {text: 'Health Check:'}); + infoEl.createEl('code', {text: `http://127.0.0.1:${this.plugin.settings.port}/health`}); + } + } +} diff --git a/old-structure/mcp-server.ts b/old-structure/mcp-server.ts new file mode 100644 index 0000000..3f3d395 --- /dev/null +++ b/old-structure/mcp-server.ts @@ -0,0 +1,485 @@ +import { App, TFile, TFolder } from 'obsidian'; +import express, { Express, Request, Response } from 'express'; +import cors from 'cors'; +import { Server } from 'http'; +import { + JSONRPCRequest, + JSONRPCResponse, + JSONRPCError, + InitializeResult, + ListToolsResult, + CallToolResult, + Tool, + ErrorCodes, + ContentBlock +} from './mcp-types'; + +export interface MCPServerSettings { + port: number; + enableCORS: boolean; + allowedOrigins: string[]; + apiKey?: string; + enableAuth: boolean; +} + +export class MCPServer { + private app: Express; + private server: Server | null = null; + private obsidianApp: App; + private settings: MCPServerSettings; + + constructor(obsidianApp: App, settings: MCPServerSettings) { + this.obsidianApp = obsidianApp; + this.settings = settings; + this.app = express(); + this.setupMiddleware(); + this.setupRoutes(); + } + + private setupMiddleware(): void { + // Parse JSON bodies + this.app.use(express.json()); + + // CORS configuration + if (this.settings.enableCORS) { + const corsOptions = { + origin: (origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) => { + // Allow requests with no origin (like mobile apps or curl requests) + if (!origin) return callback(null, true); + + if (this.settings.allowedOrigins.includes('*') || + this.settings.allowedOrigins.includes(origin)) { + callback(null, true); + } else { + callback(new Error('Not allowed by CORS')); + } + }, + credentials: true + }; + this.app.use(cors(corsOptions)); + } + + // Authentication middleware + if (this.settings.enableAuth && this.settings.apiKey) { + this.app.use((req: Request, res: Response, next: any) => { + const authHeader = req.headers.authorization; + const apiKey = authHeader?.replace('Bearer ', ''); + + if (apiKey !== this.settings.apiKey) { + return res.status(401).json(this.createErrorResponse(null, ErrorCodes.InvalidRequest, 'Unauthorized')); + } + next(); + }); + } + + // Origin validation for security (DNS rebinding protection) + this.app.use((req: Request, res: Response, next: any) => { + const origin = req.headers.origin; + const host = req.headers.host; + + // Only allow localhost connections + if (host && !host.startsWith('localhost') && !host.startsWith('127.0.0.1')) { + return res.status(403).json(this.createErrorResponse(null, ErrorCodes.InvalidRequest, 'Only localhost connections allowed')); + } + + next(); + }); + } + + private setupRoutes(): void { + // Main MCP endpoint + this.app.post('/mcp', async (req: Request, res: Response) => { + try { + const request = req.body as JSONRPCRequest; + const response = await this.handleRequest(request); + res.json(response); + } catch (error) { + console.error('MCP request error:', error); + res.status(500).json(this.createErrorResponse(null, ErrorCodes.InternalError, 'Internal server error')); + } + }); + + // Health check endpoint + this.app.get('/health', (_req: Request, res: Response) => { + res.json({ status: 'ok', timestamp: Date.now() }); + }); + } + + private async handleRequest(request: JSONRPCRequest): Promise { + try { + switch (request.method) { + case 'initialize': + return this.createSuccessResponse(request.id, await this.handleInitialize(request.params)); + case 'tools/list': + return this.createSuccessResponse(request.id, await this.handleListTools()); + case 'tools/call': + return this.createSuccessResponse(request.id, await this.handleCallTool(request.params)); + case 'ping': + return this.createSuccessResponse(request.id, {}); + default: + return this.createErrorResponse(request.id, ErrorCodes.MethodNotFound, `Method not found: ${request.method}`); + } + } catch (error) { + console.error('Error handling request:', error); + return this.createErrorResponse(request.id, ErrorCodes.InternalError, error.message); + } + } + + private async handleInitialize(params: any): Promise { + return { + protocolVersion: "2024-11-05", + capabilities: { + tools: {} + }, + serverInfo: { + name: "obsidian-mcp-server", + version: "1.0.0" + } + }; + } + + private async handleListTools(): Promise { + const tools: Tool[] = [ + { + name: "read_note", + description: "Read the content of a note from the Obsidian vault", + inputSchema: { + type: "object", + properties: { + path: { + type: "string", + description: "Path to the note within the vault (e.g., 'folder/note.md')" + } + }, + required: ["path"] + } + }, + { + name: "create_note", + description: "Create a new note in the Obsidian vault", + inputSchema: { + type: "object", + properties: { + path: { + type: "string", + description: "Path for the new note (e.g., 'folder/note.md')" + }, + content: { + type: "string", + description: "Content of the note" + } + }, + required: ["path", "content"] + } + }, + { + name: "update_note", + description: "Update an existing note in the Obsidian vault", + inputSchema: { + type: "object", + properties: { + path: { + type: "string", + description: "Path to the note to update" + }, + content: { + type: "string", + description: "New content for the note" + } + }, + required: ["path", "content"] + } + }, + { + name: "delete_note", + description: "Delete a note from the Obsidian vault", + inputSchema: { + type: "object", + properties: { + path: { + type: "string", + description: "Path to the note to delete" + } + }, + required: ["path"] + } + }, + { + name: "search_notes", + description: "Search for notes in the Obsidian vault", + inputSchema: { + type: "object", + properties: { + query: { + type: "string", + description: "Search query string" + } + }, + required: ["query"] + } + }, + { + name: "get_vault_info", + description: "Get information about the Obsidian vault", + inputSchema: { + type: "object", + properties: {} + } + }, + { + name: "list_notes", + description: "List all notes in the vault or in a specific folder", + inputSchema: { + type: "object", + properties: { + folder: { + type: "string", + description: "Optional folder path to list notes from" + } + } + } + } + ]; + + return { tools }; + } + + private async handleCallTool(params: any): Promise { + const { name, arguments: args } = params; + + try { + switch (name) { + case "read_note": + return await this.readNote(args.path); + case "create_note": + return await this.createNote(args.path, args.content); + case "update_note": + return await this.updateNote(args.path, args.content); + case "delete_note": + return await this.deleteNote(args.path); + case "search_notes": + return await this.searchNotes(args.query); + case "get_vault_info": + return await this.getVaultInfo(); + case "list_notes": + return await this.listNotes(args.folder); + default: + return { + content: [{ type: "text", text: `Unknown tool: ${name}` }], + isError: true + }; + } + } catch (error) { + return { + content: [{ type: "text", text: `Error: ${error.message}` }], + isError: true + }; + } + } + + // Tool implementations + + private async readNote(path: string): Promise { + const file = this.obsidianApp.vault.getAbstractFileByPath(path); + + if (!file || !(file instanceof TFile)) { + return { + content: [{ type: "text", text: `Note not found: ${path}` }], + isError: true + }; + } + + const content = await this.obsidianApp.vault.read(file); + return { + content: [{ type: "text", text: content }] + }; + } + + private async createNote(path: string, content: string): Promise { + try { + const file = await this.obsidianApp.vault.create(path, content); + return { + content: [{ type: "text", text: `Note created successfully: ${file.path}` }] + }; + } catch (error) { + return { + content: [{ type: "text", text: `Failed to create note: ${error.message}` }], + isError: true + }; + } + } + + private async updateNote(path: string, content: string): Promise { + const file = this.obsidianApp.vault.getAbstractFileByPath(path); + + if (!file || !(file instanceof TFile)) { + return { + content: [{ type: "text", text: `Note not found: ${path}` }], + isError: true + }; + } + + await this.obsidianApp.vault.modify(file, content); + return { + content: [{ type: "text", text: `Note updated successfully: ${path}` }] + }; + } + + private async deleteNote(path: string): Promise { + const file = this.obsidianApp.vault.getAbstractFileByPath(path); + + if (!file || !(file instanceof TFile)) { + return { + content: [{ type: "text", text: `Note not found: ${path}` }], + isError: true + }; + } + + await this.obsidianApp.vault.delete(file); + return { + content: [{ type: "text", text: `Note deleted successfully: ${path}` }] + }; + } + + private async searchNotes(query: string): Promise { + const files = this.obsidianApp.vault.getMarkdownFiles(); + const results: string[] = []; + + for (const file of files) { + const content = await this.obsidianApp.vault.read(file); + if (content.toLowerCase().includes(query.toLowerCase()) || + file.basename.toLowerCase().includes(query.toLowerCase())) { + results.push(file.path); + } + } + + return { + content: [{ + type: "text", + text: results.length > 0 + ? `Found ${results.length} notes:\n${results.join('\n')}` + : 'No notes found matching the query' + }] + }; + } + + private async getVaultInfo(): Promise { + const files = this.obsidianApp.vault.getFiles(); + const markdownFiles = this.obsidianApp.vault.getMarkdownFiles(); + + const info = { + name: this.obsidianApp.vault.getName(), + totalFiles: files.length, + markdownFiles: markdownFiles.length, + rootPath: (this.obsidianApp.vault.adapter as any).basePath || 'Unknown' + }; + + return { + content: [{ + type: "text", + text: JSON.stringify(info, null, 2) + }] + }; + } + + private async listNotes(folder?: string): Promise { + let files: TFile[]; + + if (folder) { + const folderObj = this.obsidianApp.vault.getAbstractFileByPath(folder); + if (!folderObj || !(folderObj instanceof TFolder)) { + return { + content: [{ type: "text", text: `Folder not found: ${folder}` }], + isError: true + }; + } + files = []; + this.obsidianApp.vault.getMarkdownFiles().forEach((file: TFile) => { + if (file.path.startsWith(folder + '/')) { + files.push(file); + } + }); + } else { + files = this.obsidianApp.vault.getMarkdownFiles(); + } + + const noteList = files.map(f => f.path).join('\n'); + return { + content: [{ + type: "text", + text: `Found ${files.length} notes:\n${noteList}` + }] + }; + } + + // Helper methods + + private createSuccessResponse(id: string | number | undefined, result: any): JSONRPCResponse { + return { + jsonrpc: "2.0", + id: id ?? null, + result + }; + } + + private createErrorResponse(id: string | number | undefined | null, code: number, message: string, data?: any): JSONRPCResponse { + return { + jsonrpc: "2.0", + id: id ?? null, + error: { + code, + message, + data + } + }; + } + + // Server lifecycle + + public async start(): Promise { + return new Promise((resolve, reject) => { + try { + this.server = this.app.listen(this.settings.port, '127.0.0.1', () => { + console.log(`MCP Server listening on http://127.0.0.1:${this.settings.port}/mcp`); + resolve(); + }); + + this.server.on('error', (error: any) => { + if (error.code === 'EADDRINUSE') { + reject(new Error(`Port ${this.settings.port} is already in use`)); + } else { + reject(error); + } + }); + } catch (error) { + reject(error); + } + }); + } + + public async stop(): Promise { + return new Promise((resolve, reject) => { + if (this.server) { + this.server.close((err?: Error) => { + if (err) { + reject(err); + } else { + console.log('MCP Server stopped'); + this.server = null; + resolve(); + } + }); + } else { + resolve(); + } + }); + } + + public isRunning(): boolean { + return this.server !== null; + } + + public updateSettings(settings: MCPServerSettings): void { + this.settings = settings; + } +} diff --git a/old-structure/mcp-types.ts b/old-structure/mcp-types.ts new file mode 100644 index 0000000..1862808 --- /dev/null +++ b/old-structure/mcp-types.ts @@ -0,0 +1,122 @@ +// MCP Protocol Types based on JSON-RPC 2.0 + +export interface JSONRPCRequest { + jsonrpc: "2.0"; + id?: string | number; + method: string; + params?: any; +} + +export interface JSONRPCResponse { + jsonrpc: "2.0"; + id: string | number | null; + result?: any; + error?: JSONRPCError; +} + +export interface JSONRPCError { + code: number; + message: string; + data?: any; +} + +export interface JSONRPCNotification { + jsonrpc: "2.0"; + method: string; + params?: any; +} + +// MCP Protocol Messages + +export interface InitializeRequest { + method: "initialize"; + params: { + protocolVersion: string; + capabilities: ClientCapabilities; + clientInfo: { + name: string; + version: string; + }; + }; +} + +export interface InitializeResult { + protocolVersion: string; + capabilities: ServerCapabilities; + serverInfo: { + name: string; + version: string; + }; +} + +export interface ClientCapabilities { + roots?: { + listChanged?: boolean; + }; + sampling?: {}; + experimental?: Record; +} + +export interface ServerCapabilities { + tools?: {}; + resources?: { + subscribe?: boolean; + listChanged?: boolean; + }; + prompts?: { + listChanged?: boolean; + }; + logging?: {}; + experimental?: Record; +} + +export interface ListToolsRequest { + method: "tools/list"; + params?: { + cursor?: string; + }; +} + +export interface Tool { + name: string; + description?: string; + inputSchema: { + type: "object"; + properties?: Record; + required?: string[]; + }; +} + +export interface ListToolsResult { + tools: Tool[]; + nextCursor?: string; +} + +export interface CallToolRequest { + method: "tools/call"; + params: { + name: string; + arguments?: Record; + }; +} + +export interface CallToolResult { + content: ContentBlock[]; + isError?: boolean; +} + +export interface ContentBlock { + type: "text" | "image" | "resource"; + text?: string; + data?: string; + mimeType?: string; +} + +// Error codes +export const ErrorCodes = { + ParseError: -32700, + InvalidRequest: -32600, + MethodNotFound: -32601, + InvalidParams: -32602, + InternalError: -32603, +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5d74b75 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3400 @@ +{ + "name": "obsidian-mcp-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "obsidian-mcp-server", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "cors": "^2.8.5", + "express": "^4.18.2" + }, + "devDependencies": { + "@types/body-parser": "^1.19.6", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/node": "^16.11.6", + "@typescript-eslint/eslint-plugin": "5.29.0", + "@typescript-eslint/parser": "5.29.0", + "builtin-modules": "3.3.0", + "esbuild": "0.17.3", + "obsidian": "latest", + "tslib": "2.4.0", + "typescript": "4.7.4" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz", + "integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.38.1", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz", + "integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.3.tgz", + "integrity": "sha512-1Mlz934GvbgdDmt26rTLmf03cAgLg5HyOgJN+ZGCeP3Q9ynYTNMn2/LQxIl7Uy+o4K6Rfi2OuLsr12JQQR8gNg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.3.tgz", + "integrity": "sha512-XvJsYo3dO3Pi4kpalkyMvfQsjxPWHYjoX4MDiB/FUM4YMfWcXa5l4VCwFWVYI1+92yxqjuqrhNg0CZg3gSouyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.3.tgz", + "integrity": "sha512-nuV2CmLS07Gqh5/GrZLuqkU9Bm6H6vcCspM+zjp9TdQlxJtIe+qqEXQChmfc7nWdyr/yz3h45Utk1tUn8Cz5+A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.3.tgz", + "integrity": "sha512-01Hxaaat6m0Xp9AXGM8mjFtqqwDjzlMP0eQq9zll9U85ttVALGCGDuEvra5Feu/NbP5AEP1MaopPwzsTcUq1cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.3.tgz", + "integrity": "sha512-Eo2gq0Q/er2muf8Z83X21UFoB7EU6/m3GNKvrhACJkjVThd0uA+8RfKpfNhuMCl1bKRfBzKOk6xaYKQZ4lZqvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.3.tgz", + "integrity": "sha512-CN62ESxaquP61n1ZjQP/jZte8CE09M6kNn3baos2SeUfdVBkWN5n6vGp2iKyb/bm/x4JQzEvJgRHLGd5F5b81w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.3.tgz", + "integrity": "sha512-feq+K8TxIznZE+zhdVurF3WNJ/Sa35dQNYbaqM/wsCbWdzXr5lyq+AaTUSER2cUR+SXPnd/EY75EPRjf4s1SLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.3.tgz", + "integrity": "sha512-CLP3EgyNuPcg2cshbwkqYy5bbAgK+VhyfMU7oIYyn+x4Y67xb5C5ylxsNUjRmr8BX+MW3YhVNm6Lq6FKtRTWHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.3.tgz", + "integrity": "sha512-JHeZXD4auLYBnrKn6JYJ0o5nWJI9PhChA/Nt0G4MvLaMrvXuWnY93R3a7PiXeJQphpL1nYsaMcoV2QtuvRnF/g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.3.tgz", + "integrity": "sha512-FyXlD2ZjZqTFh0sOQxFDiWG1uQUEOLbEh9gKN/7pFxck5Vw0qjWSDqbn6C10GAa1rXJpwsntHcmLqydY9ST9ZA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.3.tgz", + "integrity": "sha512-OrDGMvDBI2g7s04J8dh8/I7eSO+/E7nMDT2Z5IruBfUO/RiigF1OF6xoH33Dn4W/OwAWSUf1s2nXamb28ZklTA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.3.tgz", + "integrity": "sha512-DcnUpXnVCJvmv0TzuLwKBC2nsQHle8EIiAJiJ+PipEVC16wHXaPEKP0EqN8WnBe0TPvMITOUlP2aiL5YMld+CQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.3.tgz", + "integrity": "sha512-BDYf/l1WVhWE+FHAW3FzZPtVlk9QsrwsxGzABmN4g8bTjmhazsId3h127pliDRRu5674k1Y2RWejbpN46N9ZhQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.3.tgz", + "integrity": "sha512-WViAxWYMRIi+prTJTyV1wnqd2mS2cPqJlN85oscVhXdb/ZTFJdrpaqm/uDsZPGKHtbg5TuRX/ymKdOSk41YZow==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.3.tgz", + "integrity": "sha512-Iw8lkNHUC4oGP1O/KhumcVy77u2s6+KUjieUqzEU3XuWJqZ+AY7uVMrrCbAiwWTkpQHkr00BuXH5RpC6Sb/7Ug==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.3.tgz", + "integrity": "sha512-0AGkWQMzeoeAtXQRNB3s4J1/T2XbigM2/Mn2yU1tQSmQRmHIZdkGbVq2A3aDdNslPyhb9/lH0S5GMTZ4xsjBqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.3.tgz", + "integrity": "sha512-4+rR/WHOxIVh53UIQIICryjdoKdHsFZFD4zLSonJ9RRw7bhKzVyXbnRPsWSfwybYqw9sB7ots/SYyufL1mBpEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.3.tgz", + "integrity": "sha512-cVpWnkx9IYg99EjGxa5Gc0XmqumtAwK3aoz7O4Dii2vko+qXbkHoujWA68cqXjhh6TsLaQelfDO4MVnyr+ODeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.3.tgz", + "integrity": "sha512-RxmhKLbTCDAY2xOfrww6ieIZkZF+KBqG7S2Ako2SljKXRFi+0863PspK74QQ7JpmWwncChY25JTJSbVBYGQk2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.3.tgz", + "integrity": "sha512-0r36VeEJ4efwmofxVJRXDjVRP2jTmv877zc+i+Pc7MNsIr38NfsjkQj23AfF7l0WbB+RQ7VUb+LDiqC/KY/M/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.3.tgz", + "integrity": "sha512-wgO6rc7uGStH22nur4aLFcq7Wh86bE9cOFmfTr/yxN3BXvDEdCSXyKkO+U5JIt53eTOgC47v9k/C1bITWL/Teg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.3.tgz", + "integrity": "sha512-FdVl64OIuiKjgXBjwZaJLKp0eaEckifbhn10dXWhysMJkWblg3OEEGKSIyhiD5RSgAya8WzP3DNkngtIg3Nt7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/codemirror": { + "version": "5.60.8", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz", + "integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/tern": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", + "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", + "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "16.18.126", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz", + "integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.0.tgz", + "integrity": "sha512-zBF6vZJn1IaMpg3xUF25VK3gd3l8zwE0ZLRX7dsQyQi+jp4E8mMDJNGDYnYse+bQhYwWERTxVwHpi3dMOq7RKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.9.tgz", + "integrity": "sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/tern": { + "version": "0.23.9", + "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", + "integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.29.0.tgz", + "integrity": "sha512-kgTsISt9pM53yRFQmLZ4npj99yGl3x3Pl7z4eA66OuTzAGC4bQB5H5fuLwPnqTKU3yyrrg4MIhjF17UYnL4c0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "5.29.0", + "@typescript-eslint/type-utils": "5.29.0", + "@typescript-eslint/utils": "5.29.0", + "debug": "^4.3.4", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.2.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.29.0.tgz", + "integrity": "sha512-ruKWTv+x0OOxbzIw9nW5oWlUopvP/IQDjB5ZqmTglLIoDTctLlAJpAQFpNPJP/ZI7hTT9sARBosEfaKbcFuECw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "5.29.0", + "@typescript-eslint/types": "5.29.0", + "@typescript-eslint/typescript-estree": "5.29.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.29.0.tgz", + "integrity": "sha512-etbXUT0FygFi2ihcxDZjz21LtC+Eps9V2xVx09zFoN44RRHPrkMflidGMI+2dUs821zR1tDS6Oc9IXxIjOUZwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.29.0", + "@typescript-eslint/visitor-keys": "5.29.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.29.0.tgz", + "integrity": "sha512-JK6bAaaiJozbox3K220VRfCzLa9n0ib/J+FHIwnaV3Enw/TO267qe0pM1b1QrrEuy6xun374XEAsRlA86JJnyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "5.29.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.29.0.tgz", + "integrity": "sha512-X99VbqvAXOMdVyfFmksMy3u8p8yoRGITgU1joBJPzeYa0rhdf5ok9S56/itRoUSh99fiDoMtarSIJXo7H/SnOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.29.0.tgz", + "integrity": "sha512-mQvSUJ/JjGBdvo+1LwC+GY2XmSYjK1nAaVw2emp/E61wEVYEyibRHCqm1I1vEKbXCpUKuW4G7u9ZCaZhJbLoNQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "5.29.0", + "@typescript-eslint/visitor-keys": "5.29.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.29.0.tgz", + "integrity": "sha512-3Eos6uP1nyLOBayc/VUdKZikV90HahXE5Dx9L5YlSd/7ylQPXhLk1BYb29SDgnBnTp+jmSZUU0QxUiyHgW4p7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.29.0", + "@typescript-eslint/types": "5.29.0", + "@typescript-eslint/typescript-estree": "5.29.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.29.0.tgz", + "integrity": "sha512-Hpb/mCWsjILvikMQoZIE3voc9wtQcS0A9FUw3h8bhr9UxBdtI/tw1ZDZUOXHXLOVMedKCH5NxyzATwnU78bWCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.29.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0", + "peer": true + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.3.tgz", + "integrity": "sha512-9n3AsBRe6sIyOc6kmoXg2ypCLgf3eZSraWFRpnkto+svt8cZNuKTkb1bhQcitBcvIqjNiK7K0J3KPmwGSfkA8g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.3", + "@esbuild/android-arm64": "0.17.3", + "@esbuild/android-x64": "0.17.3", + "@esbuild/darwin-arm64": "0.17.3", + "@esbuild/darwin-x64": "0.17.3", + "@esbuild/freebsd-arm64": "0.17.3", + "@esbuild/freebsd-x64": "0.17.3", + "@esbuild/linux-arm": "0.17.3", + "@esbuild/linux-arm64": "0.17.3", + "@esbuild/linux-ia32": "0.17.3", + "@esbuild/linux-loong64": "0.17.3", + "@esbuild/linux-mips64el": "0.17.3", + "@esbuild/linux-ppc64": "0.17.3", + "@esbuild/linux-riscv64": "0.17.3", + "@esbuild/linux-s390x": "0.17.3", + "@esbuild/linux-x64": "0.17.3", + "@esbuild/netbsd-x64": "0.17.3", + "@esbuild/openbsd-x64": "0.17.3", + "@esbuild/sunos-x64": "0.17.3", + "@esbuild/win32-arm64": "0.17.3", + "@esbuild/win32-ia32": "0.17.3", + "@esbuild/win32-x64": "0.17.3" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obsidian": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.10.0.tgz", + "integrity": "sha512-F7hhnmGRQD1TanDPFT//LD3iKNUVd7N8sKL7flCCHRszfTxpDJ39j3T7LHbcGpyid906i6lD5oO+cnfLBzJMKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/codemirror": "5.60.8", + "moment": "2.29.4" + }, + "peerDependencies": { + "@codemirror/state": "6.5.0", + "@codemirror/view": "6.38.1" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6fc0d78 --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "name": "obsidian-mcp-server", + "version": "1.0.0", + "description": "MCP (Model Context Protocol) server plugin for Obsidian - exposes vault operations via HTTP", + "main": "main.js", + "scripts": { + "dev": "node esbuild.config.mjs", + "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", + "version": "node version-bump.mjs && git add manifest.json versions.json" + }, + "keywords": [ + "obsidian", + "mcp", + "model-context-protocol", + "ai", + "llm" + ], + "author": "", + "license": "MIT", + "dependencies": { + "cors": "^2.8.5", + "express": "^4.18.2" + }, + "devDependencies": { + "@types/body-parser": "^1.19.6", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/node": "^16.11.6", + "@typescript-eslint/eslint-plugin": "5.29.0", + "@typescript-eslint/parser": "5.29.0", + "builtin-modules": "3.3.0", + "esbuild": "0.17.3", + "obsidian": "latest", + "tslib": "2.4.0", + "typescript": "4.7.4" + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..6577136 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,123 @@ +import { Notice, Plugin } from 'obsidian'; +import { MCPServer } from './server/mcp-server'; +import { MCPPluginSettings, DEFAULT_SETTINGS } from './types/settings-types'; +import { MCPServerSettingTab } from './settings'; + +export default class MCPServerPlugin extends Plugin { + settings!: MCPPluginSettings; + mcpServer: MCPServer | null = null; + statusBarItem: HTMLElement | null = null; + + async onload() { + await this.loadSettings(); + + // Add status bar item + this.statusBarItem = this.addStatusBarItem(); + this.updateStatusBar(); + + // Add ribbon icon to toggle server + this.addRibbonIcon('server', 'Toggle MCP Server', async () => { + if (this.mcpServer?.isRunning()) { + await this.stopServer(); + } else { + await this.startServer(); + } + }); + + // Register commands + this.addCommand({ + id: 'start-mcp-server', + name: 'Start MCP Server', + callback: async () => { + await this.startServer(); + } + }); + + this.addCommand({ + id: 'stop-mcp-server', + name: 'Stop MCP Server', + callback: async () => { + await this.stopServer(); + } + }); + + this.addCommand({ + id: 'restart-mcp-server', + name: 'Restart MCP Server', + callback: async () => { + await this.stopServer(); + await this.startServer(); + } + }); + + // Add settings tab + this.addSettingTab(new MCPServerSettingTab(this.app, this)); + + // Auto-start if enabled + if (this.settings.autoStart) { + await this.startServer(); + } + } + + async onunload() { + await this.stopServer(); + } + + async startServer() { + if (this.mcpServer?.isRunning()) { + new Notice('MCP Server is already running'); + return; + } + + try { + this.mcpServer = new MCPServer(this.app, this.settings); + await this.mcpServer.start(); + new Notice(`MCP Server started on port ${this.settings.port}`); + this.updateStatusBar(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + new Notice(`Failed to start MCP Server: ${message}`); + console.error('MCP Server start error:', error); + } + } + + async stopServer() { + if (!this.mcpServer?.isRunning()) { + new Notice('MCP Server is not running'); + return; + } + + try { + await this.mcpServer.stop(); + new Notice('MCP Server stopped'); + this.updateStatusBar(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + new Notice(`Failed to stop MCP Server: ${message}`); + console.error('MCP Server stop error:', error); + } + } + + updateStatusBar() { + if (this.statusBarItem) { + const isRunning = this.mcpServer?.isRunning() ?? false; + this.statusBarItem.setText( + isRunning + ? `MCP: Running (${this.settings.port})` + : 'MCP: Stopped' + ); + this.statusBarItem.addClass('mcp-status-bar'); + } + } + + async loadSettings() { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + } + + async saveSettings() { + await this.saveData(this.settings); + if (this.mcpServer) { + this.mcpServer.updateSettings(this.settings); + } + } +} diff --git a/src/server/mcp-server.ts b/src/server/mcp-server.ts new file mode 100644 index 0000000..f67beb0 --- /dev/null +++ b/src/server/mcp-server.ts @@ -0,0 +1,144 @@ +import { App } from 'obsidian'; +import express, { Express } from 'express'; +import { Server } from 'http'; +import { + JSONRPCRequest, + JSONRPCResponse, + ErrorCodes, + InitializeResult, + ListToolsResult, + CallToolResult +} from '../types/mcp-types'; +import { MCPServerSettings } from '../types/settings-types'; +import { ToolRegistry } from '../tools'; +import { setupMiddleware } from './middleware'; +import { setupRoutes } from './routes'; + +export class MCPServer { + private app: Express; + private server: Server | null = null; + private obsidianApp: App; + private settings: MCPServerSettings; + private toolRegistry: ToolRegistry; + + constructor(obsidianApp: App, settings: MCPServerSettings) { + this.obsidianApp = obsidianApp; + this.settings = settings; + this.app = express(); + this.toolRegistry = new ToolRegistry(obsidianApp); + + setupMiddleware(this.app, this.settings, this.createErrorResponse.bind(this)); + setupRoutes(this.app, this.handleRequest.bind(this), this.createErrorResponse.bind(this)); + } + + private async handleRequest(request: JSONRPCRequest): Promise { + try { + switch (request.method) { + case 'initialize': + return this.createSuccessResponse(request.id, await this.handleInitialize(request.params)); + case 'tools/list': + return this.createSuccessResponse(request.id, await this.handleListTools()); + case 'tools/call': + return this.createSuccessResponse(request.id, await this.handleCallTool(request.params)); + case 'ping': + return this.createSuccessResponse(request.id, {}); + default: + return this.createErrorResponse(request.id, ErrorCodes.MethodNotFound, `Method not found: ${request.method}`); + } + } catch (error) { + console.error('Error handling request:', error); + return this.createErrorResponse(request.id, ErrorCodes.InternalError, (error as Error).message); + } + } + + private async handleInitialize(_params: any): Promise { + return { + protocolVersion: "2024-11-05", + capabilities: { + tools: {} + }, + serverInfo: { + name: "obsidian-mcp-server", + version: "1.0.0" + } + }; + } + + private async handleListTools(): Promise { + return { + tools: this.toolRegistry.getToolDefinitions() + }; + } + + private async handleCallTool(params: any): Promise { + const { name, arguments: args } = params; + return await this.toolRegistry.callTool(name, args); + } + + private createSuccessResponse(id: string | number | undefined, result: any): JSONRPCResponse { + return { + jsonrpc: "2.0", + id: id ?? null, + result + }; + } + + private createErrorResponse(id: string | number | undefined | null, code: number, message: string, data?: any): JSONRPCResponse { + return { + jsonrpc: "2.0", + id: id ?? null, + error: { + code, + message, + data + } + }; + } + + public async start(): Promise { + return new Promise((resolve, reject) => { + try { + this.server = this.app.listen(this.settings.port, '127.0.0.1', () => { + console.log(`MCP Server listening on http://127.0.0.1:${this.settings.port}/mcp`); + resolve(); + }); + + this.server.on('error', (error: any) => { + if (error.code === 'EADDRINUSE') { + reject(new Error(`Port ${this.settings.port} is already in use`)); + } else { + reject(error); + } + }); + } catch (error) { + reject(error); + } + }); + } + + public async stop(): Promise { + return new Promise((resolve, reject) => { + if (this.server) { + this.server.close((err?: Error) => { + if (err) { + reject(err); + } else { + console.log('MCP Server stopped'); + this.server = null; + resolve(); + } + }); + } else { + resolve(); + } + }); + } + + public isRunning(): boolean { + return this.server !== null; + } + + public updateSettings(settings: MCPServerSettings): void { + this.settings = settings; + } +} diff --git a/src/server/middleware.ts b/src/server/middleware.ts new file mode 100644 index 0000000..9bed5ce --- /dev/null +++ b/src/server/middleware.ts @@ -0,0 +1,54 @@ +import { Express, Request, Response } from 'express'; +import express from 'express'; +import cors from 'cors'; +import { MCPServerSettings } from '../types/settings-types'; +import { ErrorCodes } from '../types/mcp-types'; + +export function setupMiddleware(app: Express, settings: MCPServerSettings, createErrorResponse: (id: any, code: number, message: string) => any): void { + // Parse JSON bodies + app.use(express.json()); + + // CORS configuration + if (settings.enableCORS) { + const corsOptions = { + origin: (origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) => { + // Allow requests with no origin (like mobile apps or curl requests) + if (!origin) return callback(null, true); + + if (settings.allowedOrigins.includes('*') || + settings.allowedOrigins.includes(origin)) { + callback(null, true); + } else { + callback(new Error('Not allowed by CORS')); + } + }, + credentials: true + }; + app.use(cors(corsOptions)); + } + + // Authentication middleware + if (settings.enableAuth && settings.apiKey) { + app.use((req: Request, res: Response, next: any) => { + const authHeader = req.headers.authorization; + const apiKey = authHeader?.replace('Bearer ', ''); + + if (apiKey !== settings.apiKey) { + return res.status(401).json(createErrorResponse(null, ErrorCodes.InvalidRequest, 'Unauthorized')); + } + next(); + }); + } + + // Origin validation for security (DNS rebinding protection) + app.use((req: Request, res: Response, next: any) => { + const host = req.headers.host; + + // Only allow localhost connections + if (host && !host.startsWith('localhost') && !host.startsWith('127.0.0.1')) { + return res.status(403).json(createErrorResponse(null, ErrorCodes.InvalidRequest, 'Only localhost connections allowed')); + } + + next(); + }); +} diff --git a/src/server/routes.ts b/src/server/routes.ts new file mode 100644 index 0000000..b924a4c --- /dev/null +++ b/src/server/routes.ts @@ -0,0 +1,25 @@ +import { Express, Request, Response } from 'express'; +import { JSONRPCRequest, JSONRPCResponse, ErrorCodes } from '../types/mcp-types'; + +export function setupRoutes( + app: Express, + handleRequest: (request: JSONRPCRequest) => Promise, + createErrorResponse: (id: any, code: number, message: string) => JSONRPCResponse +): void { + // Main MCP endpoint + app.post('/mcp', async (req: Request, res: Response) => { + try { + const request = req.body as JSONRPCRequest; + const response = await handleRequest(request); + res.json(response); + } catch (error) { + console.error('MCP request error:', error); + res.status(500).json(createErrorResponse(null, ErrorCodes.InternalError, 'Internal server error')); + } + }); + + // Health check endpoint + app.get('/health', (_req: Request, res: Response) => { + res.json({ status: 'ok', timestamp: Date.now() }); + }); +} diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 0000000..4fe9fba --- /dev/null +++ b/src/settings.ts @@ -0,0 +1,153 @@ +import { App, PluginSettingTab, Setting } from 'obsidian'; +import { MCPPluginSettings } from './types/settings-types'; +import MCPServerPlugin from './main'; + +export class MCPServerSettingTab extends PluginSettingTab { + plugin: MCPServerPlugin; + + constructor(app: App, plugin: MCPServerPlugin) { + super(app, plugin); + this.plugin = plugin; + } + + display(): void { + const {containerEl} = this; + + containerEl.empty(); + + containerEl.createEl('h2', {text: 'MCP Server Settings'}); + + // Network disclosure + const disclosureEl = containerEl.createEl('div', {cls: 'mcp-disclosure'}); + disclosureEl.createEl('p', { + text: 'โš ๏ธ This plugin runs a local HTTP server to expose vault operations via the Model Context Protocol (MCP). The server only accepts connections from localhost (127.0.0.1) for security.' + }); + disclosureEl.style.backgroundColor = 'var(--background-secondary)'; + disclosureEl.style.padding = '12px'; + disclosureEl.style.marginBottom = '16px'; + disclosureEl.style.borderRadius = '4px'; + + // Auto-start setting + new Setting(containerEl) + .setName('Auto-start server') + .setDesc('Automatically start the MCP server when Obsidian launches') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.autoStart) + .onChange(async (value) => { + this.plugin.settings.autoStart = value; + await this.plugin.saveSettings(); + })); + + // Port setting + new Setting(containerEl) + .setName('Port') + .setDesc('Port number for the HTTP server (requires restart)') + .addText(text => text + .setPlaceholder('3000') + .setValue(String(this.plugin.settings.port)) + .onChange(async (value) => { + const port = parseInt(value); + if (!isNaN(port) && port > 0 && port < 65536) { + this.plugin.settings.port = port; + await this.plugin.saveSettings(); + } + })); + + // CORS setting + new Setting(containerEl) + .setName('Enable CORS') + .setDesc('Enable Cross-Origin Resource Sharing') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.enableCORS) + .onChange(async (value) => { + this.plugin.settings.enableCORS = value; + await this.plugin.saveSettings(); + })); + + // Allowed origins + new Setting(containerEl) + .setName('Allowed origins') + .setDesc('Comma-separated list of allowed origins (* for all)') + .addText(text => text + .setPlaceholder('*') + .setValue(this.plugin.settings.allowedOrigins.join(', ')) + .onChange(async (value) => { + this.plugin.settings.allowedOrigins = value + .split(',') + .map(s => s.trim()) + .filter(s => s.length > 0); + await this.plugin.saveSettings(); + })); + + // Authentication + new Setting(containerEl) + .setName('Enable authentication') + .setDesc('Require API key for requests') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.enableAuth) + .onChange(async (value) => { + this.plugin.settings.enableAuth = value; + await this.plugin.saveSettings(); + })); + + // API Key + new Setting(containerEl) + .setName('API Key') + .setDesc('API key for authentication (Bearer token)') + .addText(text => text + .setPlaceholder('Enter API key') + .setValue(this.plugin.settings.apiKey || '') + .onChange(async (value) => { + this.plugin.settings.apiKey = value; + await this.plugin.saveSettings(); + })); + + // Server status + containerEl.createEl('h3', {text: 'Server Status'}); + + const statusEl = containerEl.createEl('div', {cls: 'mcp-server-status'}); + const isRunning = this.plugin.mcpServer?.isRunning() ?? false; + + statusEl.createEl('p', { + text: isRunning + ? `โœ… Server is running on http://127.0.0.1:${this.plugin.settings.port}/mcp` + : 'โญ• Server is stopped' + }); + + // Control buttons + const buttonContainer = containerEl.createEl('div', {cls: 'mcp-button-container'}); + + if (isRunning) { + buttonContainer.createEl('button', {text: 'Stop Server'}) + .addEventListener('click', async () => { + await this.plugin.stopServer(); + this.display(); + }); + + buttonContainer.createEl('button', {text: 'Restart Server'}) + .addEventListener('click', async () => { + await this.plugin.stopServer(); + await this.plugin.startServer(); + this.display(); + }); + } else { + buttonContainer.createEl('button', {text: 'Start Server'}) + .addEventListener('click', async () => { + await this.plugin.startServer(); + this.display(); + }); + } + + // Connection info + if (isRunning) { + containerEl.createEl('h3', {text: 'Connection Information'}); + + const infoEl = containerEl.createEl('div', {cls: 'mcp-connection-info'}); + infoEl.createEl('p', {text: 'MCP Endpoint:'}); + infoEl.createEl('code', {text: `http://127.0.0.1:${this.plugin.settings.port}/mcp`}); + + infoEl.createEl('p', {text: 'Health Check:'}); + infoEl.createEl('code', {text: `http://127.0.0.1:${this.plugin.settings.port}/health`}); + } + } +} diff --git a/src/tools/index.ts b/src/tools/index.ts new file mode 100644 index 0000000..511f76f --- /dev/null +++ b/src/tools/index.ts @@ -0,0 +1,149 @@ +import { App } from 'obsidian'; +import { Tool, CallToolResult } from '../types/mcp-types'; +import { NoteTools } from './note-tools'; +import { VaultTools } from './vault-tools'; + +export class ToolRegistry { + private noteTools: NoteTools; + private vaultTools: VaultTools; + + constructor(app: App) { + this.noteTools = new NoteTools(app); + this.vaultTools = new VaultTools(app); + } + + getToolDefinitions(): Tool[] { + return [ + { + name: "read_note", + description: "Read the content of a note from the Obsidian vault", + inputSchema: { + type: "object", + properties: { + path: { + type: "string", + description: "Path to the note within the vault (e.g., 'folder/note.md')" + } + }, + required: ["path"] + } + }, + { + name: "create_note", + description: "Create a new note in the Obsidian vault", + inputSchema: { + type: "object", + properties: { + path: { + type: "string", + description: "Path for the new note (e.g., 'folder/note.md')" + }, + content: { + type: "string", + description: "Content of the note" + } + }, + required: ["path", "content"] + } + }, + { + name: "update_note", + description: "Update an existing note in the Obsidian vault", + inputSchema: { + type: "object", + properties: { + path: { + type: "string", + description: "Path to the note to update" + }, + content: { + type: "string", + description: "New content for the note" + } + }, + required: ["path", "content"] + } + }, + { + name: "delete_note", + description: "Delete a note from the Obsidian vault", + inputSchema: { + type: "object", + properties: { + path: { + type: "string", + description: "Path to the note to delete" + } + }, + required: ["path"] + } + }, + { + name: "search_notes", + description: "Search for notes in the Obsidian vault", + inputSchema: { + type: "object", + properties: { + query: { + type: "string", + description: "Search query string" + } + }, + required: ["query"] + } + }, + { + name: "get_vault_info", + description: "Get information about the Obsidian vault", + inputSchema: { + type: "object", + properties: {} + } + }, + { + name: "list_notes", + description: "List all notes in the vault or in a specific folder", + inputSchema: { + type: "object", + properties: { + folder: { + type: "string", + description: "Optional folder path to list notes from" + } + } + } + } + ]; + } + + async callTool(name: string, args: any): Promise { + try { + switch (name) { + case "read_note": + return await this.noteTools.readNote(args.path); + case "create_note": + return await this.noteTools.createNote(args.path, args.content); + case "update_note": + return await this.noteTools.updateNote(args.path, args.content); + case "delete_note": + return await this.noteTools.deleteNote(args.path); + case "search_notes": + return await this.vaultTools.searchNotes(args.query); + case "get_vault_info": + return await this.vaultTools.getVaultInfo(); + case "list_notes": + return await this.vaultTools.listNotes(args.folder); + default: + return { + content: [{ type: "text", text: `Unknown tool: ${name}` }], + isError: true + }; + } + } catch (error) { + return { + content: [{ type: "text", text: `Error: ${(error as Error).message}` }], + isError: true + }; + } + } +} diff --git a/src/tools/note-tools.ts b/src/tools/note-tools.ts new file mode 100644 index 0000000..0361bdf --- /dev/null +++ b/src/tools/note-tools.ts @@ -0,0 +1,68 @@ +import { App, TFile } from 'obsidian'; +import { CallToolResult } from '../types/mcp-types'; + +export class NoteTools { + constructor(private app: App) {} + + async readNote(path: string): Promise { + const file = this.app.vault.getAbstractFileByPath(path); + + if (!file || !(file instanceof TFile)) { + return { + content: [{ type: "text", text: `Note not found: ${path}` }], + isError: true + }; + } + + const content = await this.app.vault.read(file); + return { + content: [{ type: "text", text: content }] + }; + } + + async createNote(path: string, content: string): Promise { + try { + const file = await this.app.vault.create(path, content); + return { + content: [{ type: "text", text: `Note created successfully: ${file.path}` }] + }; + } catch (error) { + return { + content: [{ type: "text", text: `Failed to create note: ${(error as Error).message}` }], + isError: true + }; + } + } + + async updateNote(path: string, content: string): Promise { + const file = this.app.vault.getAbstractFileByPath(path); + + if (!file || !(file instanceof TFile)) { + return { + content: [{ type: "text", text: `Note not found: ${path}` }], + isError: true + }; + } + + await this.app.vault.modify(file, content); + return { + content: [{ type: "text", text: `Note updated successfully: ${path}` }] + }; + } + + async deleteNote(path: string): Promise { + const file = this.app.vault.getAbstractFileByPath(path); + + if (!file || !(file instanceof TFile)) { + return { + content: [{ type: "text", text: `Note not found: ${path}` }], + isError: true + }; + } + + await this.app.vault.delete(file); + return { + content: [{ type: "text", text: `Note deleted successfully: ${path}` }] + }; + } +} diff --git a/src/tools/vault-tools.ts b/src/tools/vault-tools.ts new file mode 100644 index 0000000..b88c269 --- /dev/null +++ b/src/tools/vault-tools.ts @@ -0,0 +1,77 @@ +import { App, TFile, TFolder } from 'obsidian'; +import { CallToolResult } from '../types/mcp-types'; + +export class VaultTools { + constructor(private app: App) {} + + async searchNotes(query: string): Promise { + const files = this.app.vault.getMarkdownFiles(); + const results: string[] = []; + + for (const file of files) { + const content = await this.app.vault.read(file); + if (content.toLowerCase().includes(query.toLowerCase()) || + file.basename.toLowerCase().includes(query.toLowerCase())) { + results.push(file.path); + } + } + + return { + content: [{ + type: "text", + text: results.length > 0 + ? `Found ${results.length} notes:\n${results.join('\n')}` + : 'No notes found matching the query' + }] + }; + } + + async getVaultInfo(): Promise { + const files = this.app.vault.getFiles(); + const markdownFiles = this.app.vault.getMarkdownFiles(); + + const info = { + name: this.app.vault.getName(), + totalFiles: files.length, + markdownFiles: markdownFiles.length, + rootPath: (this.app.vault.adapter as any).basePath || 'Unknown' + }; + + return { + content: [{ + type: "text", + text: JSON.stringify(info, null, 2) + }] + }; + } + + async listNotes(folder?: string): Promise { + let files: TFile[]; + + if (folder) { + const folderObj = this.app.vault.getAbstractFileByPath(folder); + if (!folderObj || !(folderObj instanceof TFolder)) { + return { + content: [{ type: "text", text: `Folder not found: ${folder}` }], + isError: true + }; + } + files = []; + this.app.vault.getMarkdownFiles().forEach((file: TFile) => { + if (file.path.startsWith(folder + '/')) { + files.push(file); + } + }); + } else { + files = this.app.vault.getMarkdownFiles(); + } + + const noteList = files.map(f => f.path).join('\n'); + return { + content: [{ + type: "text", + text: `Found ${files.length} notes:\n${noteList}` + }] + }; + } +} diff --git a/src/types/mcp-types.ts b/src/types/mcp-types.ts new file mode 100644 index 0000000..4378bfd --- /dev/null +++ b/src/types/mcp-types.ts @@ -0,0 +1,63 @@ +// MCP Protocol Types +export interface JSONRPCRequest { + jsonrpc: "2.0"; + id?: string | number; + method: string; + params?: any; +} + +export interface JSONRPCResponse { + jsonrpc: "2.0"; + id: string | number | null; + result?: any; + error?: JSONRPCError; +} + +export interface JSONRPCError { + code: number; + message: string; + data?: any; +} + +export enum ErrorCodes { + ParseError = -32700, + InvalidRequest = -32600, + MethodNotFound = -32601, + InvalidParams = -32602, + InternalError = -32603 +} + +export interface InitializeResult { + protocolVersion: string; + capabilities: { + tools?: {}; + }; + serverInfo: { + name: string; + version: string; + }; +} + +export interface Tool { + name: string; + description: string; + inputSchema: { + type: string; + properties: Record; + required?: string[]; + }; +} + +export interface ListToolsResult { + tools: Tool[]; +} + +export interface ContentBlock { + type: "text"; + text: string; +} + +export interface CallToolResult { + content: ContentBlock[]; + isError?: boolean; +} diff --git a/src/types/settings-types.ts b/src/types/settings-types.ts new file mode 100644 index 0000000..6e80940 --- /dev/null +++ b/src/types/settings-types.ts @@ -0,0 +1,21 @@ +// Settings Types +export interface MCPServerSettings { + port: number; + enableCORS: boolean; + allowedOrigins: string[]; + apiKey?: string; + enableAuth: boolean; +} + +export interface MCPPluginSettings extends MCPServerSettings { + autoStart: boolean; +} + +export const DEFAULT_SETTINGS: MCPPluginSettings = { + port: 3000, + enableCORS: true, + allowedOrigins: ['*'], + apiKey: '', + enableAuth: false, + autoStart: false +}; diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..94b9c8d --- /dev/null +++ b/styles.css @@ -0,0 +1,53 @@ +/* MCP Server Plugin Styles */ + +.mcp-status-bar { + cursor: pointer; + padding: 0 8px; +} + +.mcp-server-status { + margin: 1em 0; + padding: 1em; + background-color: var(--background-secondary); + border-radius: 4px; +} + +.mcp-button-container { + display: flex; + gap: 8px; + margin: 1em 0; +} + +.mcp-button-container button { + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + background-color: var(--interactive-accent); + color: var(--text-on-accent); + border: none; +} + +.mcp-button-container button:hover { + background-color: var(--interactive-accent-hover); +} + +.mcp-connection-info { + margin: 1em 0; + padding: 1em; + background-color: var(--background-secondary); + border-radius: 4px; +} + +.mcp-connection-info code { + display: block; + margin: 0.5em 0 1em 0; + padding: 8px; + background-color: var(--background-primary); + border-radius: 4px; + font-family: var(--font-monospace); +} + +.mcp-connection-info p { + margin: 0.5em 0 0.25em 0; + font-weight: 600; +} diff --git a/test-client.js b/test-client.js new file mode 100644 index 0000000..e33d678 --- /dev/null +++ b/test-client.js @@ -0,0 +1,136 @@ +#!/usr/bin/env node + +/** + * Test client for Obsidian MCP Server + * Usage: node test-client.js [port] [api-key] + */ + +const http = require('http'); + +const PORT = process.argv[2] || 3000; +const API_KEY = process.argv[3] || ''; + +function makeRequest(method, params = {}) { + return new Promise((resolve, reject) => { + const data = JSON.stringify({ + jsonrpc: "2.0", + id: Date.now(), + method, + params + }); + + const options = { + hostname: '127.0.0.1', + port: PORT, + path: '/mcp', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': data.length + } + }; + + if (API_KEY) { + options.headers['Authorization'] = `Bearer ${API_KEY}`; + } + + const req = http.request(options, (res) => { + let body = ''; + res.on('data', (chunk) => body += chunk); + res.on('end', () => { + try { + resolve(JSON.parse(body)); + } catch (e) { + reject(new Error(`Failed to parse response: ${body}`)); + } + }); + }); + + req.on('error', reject); + req.write(data); + req.end(); + }); +} + +async function runTests() { + console.log('๐Ÿงช Testing Obsidian MCP Server\n'); + console.log(`Server: http://127.0.0.1:${PORT}/mcp`); + console.log(`API Key: ${API_KEY ? '***' : 'None'}\n`); + + try { + // Test 1: Initialize + console.log('1๏ธโƒฃ Testing initialize...'); + const initResponse = await makeRequest('initialize', { + protocolVersion: "2024-11-05", + capabilities: {}, + clientInfo: { + name: "test-client", + version: "1.0.0" + } + }); + console.log('โœ… Initialize successful'); + console.log(' Server:', initResponse.result.serverInfo.name, initResponse.result.serverInfo.version); + console.log(' Protocol:', initResponse.result.protocolVersion); + console.log(); + + // Test 2: List tools + console.log('2๏ธโƒฃ Testing tools/list...'); + const toolsResponse = await makeRequest('tools/list'); + console.log('โœ… Tools list successful'); + console.log(` Found ${toolsResponse.result.tools.length} tools:`); + toolsResponse.result.tools.forEach(tool => { + console.log(` - ${tool.name}: ${tool.description}`); + }); + console.log(); + + // Test 3: Get vault info + console.log('3๏ธโƒฃ Testing get_vault_info...'); + const vaultResponse = await makeRequest('tools/call', { + name: 'get_vault_info', + arguments: {} + }); + console.log('โœ… Vault info successful'); + const vaultInfo = JSON.parse(vaultResponse.result.content[0].text); + console.log(' Vault:', vaultInfo.name); + console.log(' Total files:', vaultInfo.totalFiles); + console.log(' Markdown files:', vaultInfo.markdownFiles); + console.log(); + + // Test 4: List notes + console.log('4๏ธโƒฃ Testing list_notes...'); + const listResponse = await makeRequest('tools/call', { + name: 'list_notes', + arguments: {} + }); + console.log('โœ… List notes successful'); + const firstLine = listResponse.result.content[0].text.split('\n')[0]; + console.log(' ' + firstLine); + console.log(); + + // Test 5: Ping + console.log('5๏ธโƒฃ Testing ping...'); + const pingResponse = await makeRequest('ping'); + console.log('โœ… Ping successful'); + console.log(); + + console.log('๐ŸŽ‰ All tests passed!'); + + } catch (error) { + console.error('โŒ Test failed:', error.message); + process.exit(1); + } +} + +// Check if server is running first +http.get(`http://127.0.0.1:${PORT}/health`, (res) => { + if (res.statusCode === 200) { + runTests(); + } else { + console.error('โŒ Server health check failed'); + process.exit(1); + } +}).on('error', () => { + console.error('โŒ Cannot connect to server. Is it running?'); + console.error(` Try: http://127.0.0.1:${PORT}/health`); + process.exit(1); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a9407c1 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "inlineSourceMap": true, + "inlineSources": true, + "module": "ESNext", + "target": "ES6", + "allowJs": true, + "strict": true, + "moduleResolution": "node", + "importHelpers": false, + "isolatedModules": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "lib": [ + "DOM", + "ES5", + "ES6", + "ES7" + ] + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/verify-plugin.sh b/verify-plugin.sh new file mode 100644 index 0000000..c95bc15 --- /dev/null +++ b/verify-plugin.sh @@ -0,0 +1,101 @@ +#!/bin/bash + +echo "๐Ÿ” Obsidian MCP Server Plugin Verification" +echo "==========================================" +echo "" + +# Check if we're in the right directory +if [ ! -f "manifest.json" ]; then + echo "โŒ Error: manifest.json not found" + echo " Run this script from the plugin directory" + exit 1 +fi + +echo "โœ… Plugin directory found" +echo "" + +# Check required files +echo "๐Ÿ“ Checking required files..." +files=("main.js" "manifest.json" "styles.css") +for file in "${files[@]}"; do + if [ -f "$file" ]; then + size=$(ls -lh "$file" | awk '{print $5}') + echo " โœ… $file ($size)" + else + echo " โŒ $file (missing)" + fi +done +echo "" + +# Check manifest.json content +echo "๐Ÿ“‹ Checking manifest.json..." +if command -v jq &> /dev/null; then + echo " ID: $(jq -r '.id' manifest.json)" + echo " Name: $(jq -r '.name' manifest.json)" + echo " Version: $(jq -r '.version' manifest.json)" + echo " Desktop Only: $(jq -r '.isDesktopOnly' manifest.json)" +else + echo " (install jq for detailed manifest info)" + cat manifest.json +fi +echo "" + +# Check if main.js is valid +echo "๐Ÿ”ง Checking main.js..." +if [ -f "main.js" ]; then + # Check if it's minified/bundled + if head -1 main.js | grep -q "GENERATED/BUNDLED"; then + echo " โœ… File appears to be bundled correctly" + else + echo " โš ๏ธ File may not be bundled correctly" + fi + + # Check for export + if grep -q "export" main.js; then + echo " โœ… Contains exports" + else + echo " โš ๏ธ No exports found (may be an issue)" + fi +else + echo " โŒ main.js not found" +fi +echo "" + +# Check node_modules +echo "๐Ÿ“ฆ Checking dependencies..." +if [ -d "node_modules" ]; then + echo " โœ… node_modules exists" + if [ -d "node_modules/express" ]; then + echo " โœ… express installed" + else + echo " โŒ express not installed" + fi + if [ -d "node_modules/cors" ]; then + echo " โœ… cors installed" + else + echo " โŒ cors not installed" + fi +else + echo " โŒ node_modules not found - run 'npm install'" +fi +echo "" + +# Summary +echo "๐Ÿ“Š Summary" +echo "==========" +if [ -f "main.js" ] && [ -f "manifest.json" ] && [ -f "styles.css" ]; then + echo "โœ… All required files present" + echo "" + echo "Next steps:" + echo "1. Restart Obsidian" + echo "2. Go to Settings โ†’ Community Plugins" + echo "3. Enable 'MCP Server'" + echo "4. Check for errors in Console (Ctrl+Shift+I)" +else + echo "โŒ Some required files are missing" + echo "" + echo "To fix:" + echo "1. Run: npm install" + echo "2. Run: npm run build" + echo "3. Restart Obsidian" +fi diff --git a/version-bump.mjs b/version-bump.mjs new file mode 100644 index 0000000..55d631f --- /dev/null +++ b/version-bump.mjs @@ -0,0 +1,17 @@ +import { readFileSync, writeFileSync } from "fs"; + +const targetVersion = process.env.npm_package_version; + +// read minAppVersion from manifest.json and bump version to target version +const manifest = JSON.parse(readFileSync("manifest.json", "utf8")); +const { minAppVersion } = manifest; +manifest.version = targetVersion; +writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); + +// update versions.json with target version and minAppVersion from manifest.json +// but only if the target version is not already in versions.json +const versions = JSON.parse(readFileSync('versions.json', 'utf8')); +if (!Object.values(versions).includes(minAppVersion)) { + versions[targetVersion] = minAppVersion; + writeFileSync('versions.json', JSON.stringify(versions, null, '\t')); +} diff --git a/versions.json b/versions.json new file mode 100644 index 0000000..26382a1 --- /dev/null +++ b/versions.json @@ -0,0 +1,3 @@ +{ + "1.0.0": "0.15.0" +}