#
tokens: 38648/50000 10/108 files (page 2/2)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 of 2. Use http://codebase.md/jacksteamdev/obsidian-mcp-tools?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .clinerules
├── .github
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.md
│   │   ├── config.yml
│   │   ├── feature_request.md
│   │   └── question.md
│   ├── pull_request_template.md
│   └── workflows
│       └── release.yml
├── .gitignore
├── .prettierrc.yaml
├── .vscode
│   └── settings.json
├── bun.lock
├── CONTRIBUTING.md
├── docs
│   ├── features
│   │   ├── mcp-server-install.md
│   │   └── prompt-requirements.md
│   ├── migration-plan.md
│   └── project-architecture.md
├── LICENSE
├── manifest.json
├── mise.toml
├── package.json
├── packages
│   ├── mcp-server
│   │   ├── .gitignore
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── scripts
│   │   │   └── install.ts
│   │   ├── src
│   │   │   ├── features
│   │   │   │   ├── core
│   │   │   │   │   └── index.ts
│   │   │   │   ├── fetch
│   │   │   │   │   ├── constants.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── services
│   │   │   │   │       ├── index.ts
│   │   │   │   │       ├── markdown.test.ts
│   │   │   │   │       └── markdown.ts
│   │   │   │   ├── local-rest-api
│   │   │   │   │   └── index.ts
│   │   │   │   ├── prompts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── smart-connections
│   │   │   │   │   └── index.ts
│   │   │   │   ├── templates
│   │   │   │   │   └── index.ts
│   │   │   │   └── version
│   │   │   │       └── index.ts
│   │   │   ├── index.ts
│   │   │   ├── shared
│   │   │   │   ├── formatMcpError.ts
│   │   │   │   ├── formatString.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── logger.ts
│   │   │   │   ├── makeRequest.ts
│   │   │   │   ├── parseTemplateParameters.test.ts
│   │   │   │   ├── parseTemplateParameters.ts
│   │   │   │   └── ToolRegistry.ts
│   │   │   └── types
│   │   │       └── global.d.ts
│   │   └── tsconfig.json
│   ├── obsidian-plugin
│   │   ├── .editorconfig
│   │   ├── .eslintignore
│   │   ├── .eslintrc
│   │   ├── .gitignore
│   │   ├── .npmrc
│   │   ├── bun.config.ts
│   │   ├── docs
│   │   │   └── openapi.yaml
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── scripts
│   │   │   ├── link.ts
│   │   │   └── zip.ts
│   │   ├── src
│   │   │   ├── features
│   │   │   │   ├── core
│   │   │   │   │   ├── components
│   │   │   │   │   │   └── SettingsTab.svelte
│   │   │   │   │   └── index.ts
│   │   │   │   └── mcp-server-install
│   │   │   │       ├── components
│   │   │   │       │   └── McpServerInstallSettings.svelte
│   │   │   │       ├── constants
│   │   │   │       │   ├── bundle-time.ts
│   │   │   │       │   └── index.ts
│   │   │   │       ├── index.ts
│   │   │   │       ├── services
│   │   │   │       │   ├── config.ts
│   │   │   │       │   ├── install.ts
│   │   │   │       │   ├── status.ts
│   │   │   │       │   └── uninstall.ts
│   │   │   │       ├── types.ts
│   │   │   │       └── utils
│   │   │   │           ├── getFileSystemAdapter.ts
│   │   │   │           └── openFolder.ts
│   │   │   ├── main.ts
│   │   │   ├── shared
│   │   │   │   ├── index.ts
│   │   │   │   └── logger.ts
│   │   │   └── types.ts
│   │   ├── svelte.config.js
│   │   └── tsconfig.json
│   ├── shared
│   │   ├── .gitignore
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── index.ts
│   │   │   ├── logger.ts
│   │   │   └── types
│   │   │       ├── index.ts
│   │   │       ├── plugin-local-rest-api.ts
│   │   │       ├── plugin-smart-connections.ts
│   │   │       ├── plugin-templater.ts
│   │   │       ├── prompts.ts
│   │   │       └── smart-search.ts
│   │   └── tsconfig.json
│   └── test-site
│       ├── .gitignore
│       ├── .npmrc
│       ├── .prettierignore
│       ├── .prettierrc
│       ├── eslint.config.js
│       ├── package.json
│       ├── postcss.config.js
│       ├── README.md
│       ├── src
│       │   ├── app.css
│       │   ├── app.d.ts
│       │   ├── app.html
│       │   ├── lib
│       │   │   └── index.ts
│       │   └── routes
│       │       ├── +layout.svelte
│       │       ├── +layout.ts
│       │       └── +page.svelte
│       ├── static
│       │   └── favicon.png
│       ├── svelte.config.js
│       ├── tailwind.config.ts
│       ├── tsconfig.json
│       └── vite.config.ts
├── patches
│   └── [email protected]
├── README.md
├── scripts
│   └── version.ts
├── SECURITY.md
└── versions.json
```

# Files

--------------------------------------------------------------------------------
/packages/obsidian-plugin/src/features/mcp-server-install/components/McpServerInstallSettings.svelte:
--------------------------------------------------------------------------------

```
  1 | <script lang="ts">
  2 |   import type McpToolsPlugin from "$/main";
  3 |   import { FULL_LOGGER_FILENAME, loadDependenciesArray } from "$/shared";
  4 |   import { Notice } from "obsidian";
  5 |   import { dirname } from "path";
  6 |   import { onMount } from "svelte";
  7 |   import {
  8 |     removeFromClaudeConfig,
  9 |     updateClaudeConfig,
 10 |   } from "../services/config";
 11 |   import { installMcpServer } from "../services/install";
 12 |   import { getInstallationStatus } from "../services/status";
 13 |   import { uninstallServer } from "../services/uninstall";
 14 |   import type { InstallationStatus } from "../types";
 15 |   import { openFolder } from "../utils/openFolder";
 16 | 
 17 |   export let plugin: McpToolsPlugin;
 18 | 
 19 |   // Dependencies and API key status
 20 |   const deps = loadDependenciesArray(plugin);
 21 | 
 22 |   // Installation status
 23 |   let status: InstallationStatus = {
 24 |     state: "not installed",
 25 |     versions: {},
 26 |   };
 27 |   onMount(async () => {
 28 |     status = await getInstallationStatus(plugin);
 29 |   });
 30 | 
 31 |   // Handle installation
 32 |   async function handleInstall() {
 33 |     try {
 34 |       const apiKey = await plugin.getLocalRestApiKey();
 35 |       if (!apiKey) {
 36 |         throw new Error("Local REST API key is not configured");
 37 |       }
 38 | 
 39 |       status = { ...status, state: "installing" };
 40 |       const installPath = await installMcpServer(plugin);
 41 | 
 42 |       // Update Claude config
 43 |       await updateClaudeConfig(plugin, installPath.path, apiKey);
 44 | 
 45 |       status = await getInstallationStatus(plugin);
 46 |     } catch (error) {
 47 |       const message =
 48 |         error instanceof Error ? error.message : "Installation failed";
 49 |       status = { ...status, state: "error", error: message };
 50 |       new Notice(message);
 51 |     }
 52 |   }
 53 | 
 54 |   // Handle uninstall
 55 |   async function handleUninstall() {
 56 |     try {
 57 |       status = { ...status, state: "installing" };
 58 |       await uninstallServer(plugin);
 59 |       await removeFromClaudeConfig();
 60 |       status = { ...status, state: "not installed" };
 61 |     } catch (error) {
 62 |       const message =
 63 |         error instanceof Error ? error.message : "Uninstallation failed";
 64 |       status = {
 65 |         ...status,
 66 |         state: "error",
 67 |         error: message,
 68 |       };
 69 |       new Notice(message);
 70 |     }
 71 |   }
 72 | </script>
 73 | 
 74 | <div class="installation-status">
 75 |   <h3>Installation status</h3>
 76 | 
 77 |   {#if status.state === "no api key"}
 78 |     <div class="error-message">Please configure the Local REST API plugin</div>
 79 |   {:else if status.state === "not installed"}
 80 |     <div class="status-message">
 81 |       MCP Server is not installed
 82 |       <button on:click={handleInstall}>Install server</button>
 83 |     </div>
 84 |   {:else if status.state === "installing"}
 85 |     <div class="status-message">Installing MCP server...</div>
 86 |   {:else if status.state === "installed"}
 87 |     <div class="status-message">
 88 |       MCP Server v{status.versions.server} is installed
 89 |       <button on:click={handleUninstall}>Uninstall</button>
 90 |     </div>
 91 |   {:else if status.state === "outdated"}
 92 |     <div class="status-message">
 93 |       Update available (v{status.versions.server} -> v{status.versions.plugin})
 94 |       <button on:click={handleInstall}>Update</button>
 95 |     </div>
 96 |   {:else if status.state === "uninstalling"}
 97 |     <div class="status-message">Uninstalling MCP server...</div>
 98 |   {:else if status.state === "error"}
 99 |     <div class="error-message">{status.error}</div>
100 |   {/if}
101 | </div>
102 | 
103 | <div class="dependencies">
104 |   <h3>Dependencies</h3>
105 | 
106 |   {#each $deps as dep (dep.id)}
107 |     <div class="dependency-item">
108 |       {#if dep.installed}
109 |         ✅ {dep.name} is installed
110 |       {:else}
111 |         ❌
112 |         {dep.name}
113 |         {dep.required ? "(Required)" : "(Optional)"}
114 |         {#if dep.url}<a href={dep.url} target="_blank">How to install?</a>{/if}
115 |       {/if}
116 |     </div>
117 |   {/each}
118 | </div>
119 | 
120 | <div class="links">
121 |   <h3>Resources</h3>
122 | 
123 |   {#if status.path}
124 |     <div class="link-item">
125 |       <!-- svelte-ignore a11y_no_static_element_interactions -->
126 |       <a on:click={() => status.dir && openFolder(status.dir)}>
127 |         Server install folder
128 |       </a>
129 |     </div>
130 |   {/if}
131 | 
132 |   <div class="link-item">
133 |     <!-- svelte-ignore a11y_no_static_element_interactions -->
134 |     <a on:click={() => openFolder(dirname(FULL_LOGGER_FILENAME))}>
135 |       Server log folder
136 |     </a>
137 |   </div>
138 | 
139 |   <div class="link-item">
140 |     <a
141 |       href="https://github.com/jacksteamdev/obsidian-mcp-tools"
142 |       target="_blank"
143 |     >
144 |       GitHub repository
145 |     </a>
146 |   </div>
147 | </div>
148 | 
149 | <style>
150 |   .error-message {
151 |     color: var(--text-error);
152 |     margin-bottom: 1em;
153 |   }
154 | 
155 |   .status-message {
156 |     margin-bottom: 1em;
157 |   }
158 | 
159 |   .dependency-item {
160 |     margin-bottom: 0.5em;
161 |   }
162 | 
163 |   .installed {
164 |     color: var(--text-success);
165 |   }
166 | 
167 |   .not-installed {
168 |     color: var(--text-muted);
169 |   }
170 | 
171 |   .link-item {
172 |     margin-bottom: 0.5em;
173 |   }
174 | 
175 |   button {
176 |     margin-left: 0.5em;
177 |   }
178 | </style>
179 | 
```

--------------------------------------------------------------------------------
/packages/obsidian-plugin/src/features/mcp-server-install/services/status.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type McpToolsPlugin from "$/main";
  2 | import { logger } from "$/shared/logger";
  3 | import { exec } from "child_process";
  4 | import fsp from "fs/promises";
  5 | import { Plugin } from "obsidian";
  6 | import path from "path";
  7 | import { clean, lt, valid } from "semver";
  8 | import { promisify } from "util";
  9 | import { BINARY_NAME } from "../constants";
 10 | import type { InstallationStatus, InstallPathInfo } from "../types";
 11 | import { getFileSystemAdapter } from "../utils/getFileSystemAdapter";
 12 | import { getPlatform } from "./install";
 13 | 
 14 | const execAsync = promisify(exec);
 15 | 
 16 | /**
 17 |  * Resolves the real path of the given file path, handling cases where the path is a symlink.
 18 |  *
 19 |  * @param filepath - The file path to resolve.
 20 |  * @returns The real path of the file.
 21 |  * @throws {Error} If the file is not found or the symlink cannot be resolved.
 22 |  */
 23 | async function resolveSymlinks(filepath: string): Promise<string> {
 24 |   try {
 25 |     return await fsp.realpath(filepath);
 26 |   } catch (error) {
 27 |     if ((error as NodeJS.ErrnoException).code === "ENOENT") {
 28 |       const parts = path.normalize(filepath).split(path.sep);
 29 |       let resolvedParts: string[] = [];
 30 |       let skipCount = 1; // Skip first segment by default
 31 | 
 32 |       // Handle the root segment differently for Windows vs POSIX
 33 |       if (path.win32.isAbsolute(filepath)) {
 34 |         resolvedParts.push(parts[0]);
 35 |         if (parts[1] === "") {
 36 |           resolvedParts.push("");
 37 |           skipCount = 2; // Skip two segments for UNC paths
 38 |         }
 39 |       } else if (path.posix.isAbsolute(filepath)) {
 40 |         resolvedParts.push("/");
 41 |       } else {
 42 |         resolvedParts.push(parts[0]);
 43 |       }
 44 | 
 45 |       // Process remaining path segments
 46 |       for (const part of parts.slice(skipCount)) {
 47 |         const partialPath = path.join(...resolvedParts, part);
 48 |         try {
 49 |           const resolvedPath = await fsp.realpath(partialPath);
 50 |           resolvedParts = resolvedPath.split(path.sep);
 51 |         } catch (err) {
 52 |           resolvedParts.push(part);
 53 |         }
 54 |       }
 55 | 
 56 |       return path.join(...resolvedParts);
 57 |     }
 58 | 
 59 |     logger.error(`Failed to resolve symlink:`, {
 60 |       filepath,
 61 |       error: error instanceof Error ? error.message : error,
 62 |     });
 63 |     throw new Error(`Failed to resolve symlink: ${filepath}`);
 64 |   }
 65 | }
 66 | 
 67 | export async function getInstallPath(
 68 |   plugin: Plugin,
 69 | ): Promise<InstallPathInfo | { error: string }> {
 70 |   const adapter = getFileSystemAdapter(plugin);
 71 |   if ("error" in adapter) return adapter;
 72 | 
 73 |   const platform = getPlatform();
 74 |   const originalPath = path.join(
 75 |     adapter.getBasePath(),
 76 |     plugin.app.vault.configDir,
 77 |     "plugins",
 78 |     plugin.manifest.id,
 79 |     "bin",
 80 |   );
 81 |   const realDirPath = await resolveSymlinks(originalPath);
 82 |   const platformSpecificBinary = BINARY_NAME[platform];
 83 |   const realFilePath = path.join(realDirPath, platformSpecificBinary);
 84 |   return {
 85 |     dir: realDirPath,
 86 |     path: realFilePath,
 87 |     name: platformSpecificBinary,
 88 |     symlinked: originalPath === realDirPath ? undefined : originalPath,
 89 |   };
 90 | }
 91 | 
 92 | /**
 93 |  * Gets the current installation status of the MCP server
 94 |  */
 95 | export async function getInstallationStatus(
 96 |   plugin: McpToolsPlugin,
 97 | ): Promise<InstallationStatus> {
 98 |   // Verify plugin version is valid
 99 |   const pluginVersion = valid(clean(plugin.manifest.version));
100 |   if (!pluginVersion) {
101 |     logger.error("Invalid plugin version:", { plugin });
102 |     return { state: "error", versions: {} };
103 |   }
104 | 
105 |   // Check for API key
106 |   const apiKey = plugin.getLocalRestApiKey();
107 |   if (!apiKey) {
108 |     return {
109 |       state: "no api key",
110 |       versions: { plugin: pluginVersion },
111 |     };
112 |   }
113 | 
114 |   // Verify server binary is present
115 |   const installPath = await getInstallPath(plugin);
116 |   if ("error" in installPath) {
117 |     return {
118 |       state: "error",
119 |       versions: { plugin: pluginVersion },
120 |       error: installPath.error,
121 |     };
122 |   }
123 | 
124 |   try {
125 |     await fsp.access(installPath.path, fsp.constants.X_OK);
126 |   } catch (error) {
127 |     logger.error("Failed to get server version:", { installPath });
128 |     return {
129 |       state: "not installed",
130 |       ...installPath,
131 |       versions: { plugin: pluginVersion },
132 |     };
133 |   }
134 | 
135 |   // Check server binary version
136 |   let serverVersion: string | null | undefined;
137 |   try {
138 |     const versionCommand = `"${installPath.path}" --version`;
139 |     const { stdout } = await execAsync(versionCommand);
140 |     serverVersion = clean(stdout.trim());
141 |     if (!serverVersion) throw new Error("Invalid server version string");
142 |   } catch {
143 |     logger.error("Failed to get server version:", { installPath });
144 |     return {
145 |       state: "error",
146 |       ...installPath,
147 |       versions: { plugin: pluginVersion },
148 |     };
149 |   }
150 | 
151 |   return {
152 |     ...installPath,
153 |     state: lt(serverVersion, pluginVersion) ? "outdated" : "installed",
154 |     versions: { plugin: pluginVersion, server: serverVersion },
155 |   };
156 | }
157 | 
```

--------------------------------------------------------------------------------
/packages/shared/src/types/plugin-templater.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {
  2 |   App,
  3 |   type MarkdownPostProcessorContext,
  4 |   TAbstractFile,
  5 |   TFile,
  6 |   TFolder,
  7 | } from "obsidian";
  8 | 
  9 | export enum RunMode {
 10 |   CreateNewFromTemplate,
 11 |   AppendActiveFile,
 12 |   OverwriteFile,
 13 |   OverwriteActiveFile,
 14 |   DynamicProcessor,
 15 |   StartupTemplate,
 16 | }
 17 | 
 18 | export enum FunctionsMode {
 19 |   INTERNAL,
 20 |   USER_INTERNAL,
 21 | }
 22 | 
 23 | export type RunningConfig = {
 24 |   template_file: TFile | undefined;
 25 |   target_file: TFile;
 26 |   run_mode: RunMode;
 27 |   active_file?: TFile | null;
 28 | };
 29 | 
 30 | interface TemplaterFunctions {
 31 |   app: App;
 32 |   config: RunningConfig;
 33 |   date: {
 34 |     /**
 35 |      * @param format "YYYY-MM-DD"
 36 |      * @param offset
 37 |      * @param reference
 38 |      * @param reference_format
 39 |      */
 40 |     now(
 41 |       format: string,
 42 |       offset?: number | string,
 43 |       reference?: string,
 44 |       reference_format?: string,
 45 |     ): string;
 46 |     /**
 47 |      * @param format "YYYY-MM-DD"
 48 |      */
 49 |     tomorrow(format: string): string;
 50 |     /**
 51 |      * @param format "YYYY-MM-DD"
 52 |      * @param weekday
 53 |      * @param reference
 54 |      * @param reference_format
 55 |      */
 56 |     weekday(
 57 |       format: string,
 58 |       weekday: number,
 59 |       reference?: string,
 60 |       reference_format?: string,
 61 |     ): string;
 62 |     /**
 63 |      * @param format "YYYY-MM-DD"
 64 |      */
 65 |     yesterday(format?: string): string;
 66 |   };
 67 |   file: {
 68 |     content: string;
 69 |     /**
 70 |      * @param template TFile or string
 71 |      * @param filename
 72 |      * @param open_new Default: false
 73 |      * @param folder TFolder or string
 74 |      */
 75 |     create_new(
 76 |       template: TFile | string,
 77 |       filename?: string,
 78 |       open_new?: boolean,
 79 |       folder?: TFolder | string,
 80 |     ): Promise<TFile>;
 81 |     /**
 82 |      * @param format Default: "YYYY-MM-DD HH:mm"
 83 |      */
 84 |     creation_date(format?: string): string;
 85 |     /**
 86 |      * @param order
 87 |      */
 88 |     cursor(order?: number): void;
 89 |     cursor_append(content: string): void;
 90 |     exists(filepath: string): boolean;
 91 |     find_tfile(filename: string): TFile;
 92 |     /**
 93 |      * @param absolute Default: false
 94 |      */
 95 |     folder(absolute?: boolean): string;
 96 |     include(include_link: string | TFile): string;
 97 |     /**
 98 |      * @param format Default: "YYYY-MM-DD HH:mm"
 99 |      */
100 |     last_modified_date(format?: string): string;
101 |     move(new_path: string, file_to_move?: TFile): Promise<void>;
102 |     /**
103 |      * @param relative Default: false
104 |      */
105 |     path(relative?: boolean): string;
106 |     rename(new_title: string): Promise<void>;
107 |     selection(): string;
108 |     tags: string[];
109 |     title: string;
110 |   };
111 |   frontmatter: Record<string, unknown>;
112 |   hooks: {
113 |     on_all_templates_executed(cb: () => void): void;
114 |   };
115 |   system: {
116 |     /**
117 |      * Retrieves the clipboard's content.
118 |      */
119 |     clipboard(): Promise<string>;
120 | 
121 |     /**
122 |      * @param prompt_text
123 |      * @param default_value
124 |      * @param throw_on_cancel Default: false
125 |      * @param multiline Default: false
126 |      */
127 |     prompt(
128 |       prompt_text?: string,
129 |       default_value?: string,
130 |       throw_on_cancel?: boolean,
131 |       multiline?: boolean,
132 |     ): Promise<string>;
133 | 
134 |     /**
135 |      * @param text_items String array or function mapping item to string
136 |      * @param items Array of generic type T
137 |      * @param throw_on_cancel Default: false
138 |      * @param placeholder Default: ""
139 |      * @param limit Default: undefined
140 |      */
141 |     suggester<T>(
142 |       text_items: string[] | ((item: T) => string),
143 |       items: T[],
144 |       throw_on_cancel?: boolean,
145 |       placeholder?: string,
146 |       limit?: number,
147 |     ): Promise<T>;
148 |   };
149 |   web: {
150 |     /**
151 |      * Retrieves daily quote from quotes database
152 |      */
153 |     daily_quote(): Promise<string>;
154 | 
155 |     /**
156 |      * @param size Image size specification
157 |      * @param query Search query
158 |      * @param include_size Whether to include size in URL
159 |      */
160 |     random_picture(
161 |       size: string,
162 |       query: string,
163 |       include_size: boolean,
164 |     ): Promise<string>;
165 | 
166 |     /**
167 |      * @param url Full URL to request
168 |      * @param path Optional path parameter
169 |      */
170 |     request(url: string, path?: string): Promise<string>;
171 |   };
172 |   user: Record<string, unknown>;
173 | }
174 | 
175 | export interface ITemplater {
176 |   setup(): Promise<void>;
177 |   /** Generate the config required to parse a template */
178 |   create_running_config(
179 |     template_file: TFile | undefined,
180 |     target_file: TFile,
181 |     run_mode: RunMode,
182 |   ): RunningConfig;
183 |   /** I don't think this writes the file, but the config requires the file name */
184 |   read_and_parse_template(config: RunningConfig): Promise<string>;
185 |   /** I don't think this writes the file, but the config requires the file name */
186 |   parse_template(
187 |     config: RunningConfig,
188 |     template_content: string,
189 |   ): Promise<string>;
190 |   create_new_note_from_template(
191 |     template: TFile | string,
192 |     folder?: TFolder | string,
193 |     filename?: string,
194 |     open_new_note?: boolean,
195 |   ): Promise<TFile | undefined>;
196 |   append_template_to_active_file(template_file: TFile): Promise<void>;
197 |   write_template_to_file(template_file: TFile, file: TFile): Promise<void>;
198 |   overwrite_active_file_commands(): void;
199 |   overwrite_file_commands(file: TFile, active_file?: boolean): Promise<void>;
200 |   process_dynamic_templates(
201 |     el: HTMLElement,
202 |     ctx: MarkdownPostProcessorContext,
203 |   ): Promise<void>;
204 |   get_new_file_template_for_folder(folder: TFolder): string | undefined;
205 |   get_new_file_template_for_file(file: TFile): string | undefined;
206 |   execute_startup_scripts(): Promise<void>;
207 | 
208 |   on_file_creation(
209 |     templater: ITemplater,
210 |     app: App,
211 |     file: TAbstractFile,
212 |   ): Promise<void>;
213 | 
214 |   current_functions_object: TemplaterFunctions;
215 |   functions_generator: {
216 |     generate_object(
217 |       config: RunningConfig,
218 |       functions_mode?: FunctionsMode,
219 |     ): Promise<TemplaterFunctions>;
220 |   };
221 | }
222 | 
```

--------------------------------------------------------------------------------
/packages/obsidian-plugin/src/main.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { type } from "arktype";
  2 | import type { Request, Response } from "express";
  3 | import { Notice, Plugin, TFile } from "obsidian";
  4 | import { shake } from "radash";
  5 | import { lastValueFrom } from "rxjs";
  6 | import {
  7 |   jsonSearchRequest,
  8 |   LocalRestAPI,
  9 |   searchParameters,
 10 |   Templater,
 11 |   type PromptArgAccessor,
 12 |   type SearchResponse,
 13 | } from "shared";
 14 | import { setup as setupCore } from "./features/core";
 15 | import { setup as setupMcpServerInstall } from "./features/mcp-server-install";
 16 | import {
 17 |   loadLocalRestAPI,
 18 |   loadSmartSearchAPI,
 19 |   loadTemplaterAPI,
 20 |   type Dependencies,
 21 | } from "./shared";
 22 | import { logger } from "./shared/logger";
 23 | 
 24 | export default class McpToolsPlugin extends Plugin {
 25 |   private localRestApi: Dependencies["obsidian-local-rest-api"] = {
 26 |     id: "obsidian-local-rest-api",
 27 |     name: "Local REST API",
 28 |     required: true,
 29 |     installed: false,
 30 |   };
 31 | 
 32 |   async getLocalRestApiKey(): Promise<string | undefined> {
 33 |     // The API key is stored in the plugin's settings
 34 |     return this.localRestApi.plugin?.settings?.apiKey;
 35 |   }
 36 | 
 37 |   async onload() {
 38 |     // Initialize features in order
 39 |     await setupCore(this);
 40 |     await setupMcpServerInstall(this);
 41 | 
 42 |     // Check for required dependencies
 43 |     lastValueFrom(loadLocalRestAPI(this)).then((localRestApi) => {
 44 |       this.localRestApi = localRestApi;
 45 | 
 46 |       if (!this.localRestApi.api) {
 47 |         new Notice(
 48 |           `${this.manifest.name}: Local REST API plugin is required but not found. Please install it from the community plugins and restart Obsidian.`,
 49 |           0,
 50 |         );
 51 |         return;
 52 |       }
 53 | 
 54 |       // Register endpoints
 55 |       this.localRestApi.api
 56 |         .addRoute("/search/smart")
 57 |         .post(this.handleSearchRequest.bind(this));
 58 | 
 59 |       this.localRestApi.api
 60 |         .addRoute("/templates/execute")
 61 |         .post(this.handleTemplateExecution.bind(this));
 62 | 
 63 |       logger.info("MCP Tools Plugin loaded");
 64 |     });
 65 |   }
 66 | 
 67 |   private async handleTemplateExecution(req: Request, res: Response) {
 68 |     try {
 69 |       const { api: templater } = await lastValueFrom(loadTemplaterAPI(this));
 70 |       if (!templater) {
 71 |         new Notice(
 72 |           `${this.manifest.name}: Templater plugin is not available. Please install it from the community plugins.`,
 73 |           0,
 74 |         );
 75 |         logger.error("Templater plugin is not available");
 76 |         res.status(503).json({
 77 |           error: "Templater plugin is not available",
 78 |         });
 79 |         return;
 80 |       }
 81 | 
 82 |       // Validate request body
 83 |       const params = LocalRestAPI.ApiTemplateExecutionParams(req.body);
 84 | 
 85 |       if (params instanceof type.errors) {
 86 |         const response = {
 87 |           error: "Invalid request body",
 88 |           body: req.body,
 89 |           summary: params.summary,
 90 |         };
 91 |         logger.debug("Invalid request body", response);
 92 |         res.status(400).json(response);
 93 |         return;
 94 |       }
 95 | 
 96 |       // Get prompt content from vault
 97 |       const templateFile = this.app.vault.getAbstractFileByPath(params.name);
 98 |       if (!(templateFile instanceof TFile)) {
 99 |         logger.debug("Template file not found", {
100 |           params,
101 |           templateFile,
102 |         });
103 |         res.status(404).json({
104 |           error: `File not found: ${params.name}`,
105 |         });
106 |         return;
107 |       }
108 | 
109 |       const config = templater.create_running_config(
110 |         templateFile,
111 |         templateFile,
112 |         Templater.RunMode.CreateNewFromTemplate,
113 |       );
114 | 
115 |       const prompt: PromptArgAccessor = (argName: string) => {
116 |         return params.arguments[argName] ?? "";
117 |       };
118 | 
119 |       const oldGenerateObject =
120 |         templater.functions_generator.generate_object.bind(
121 |           templater.functions_generator,
122 |         );
123 | 
124 |       // Override generate_object to inject arg into user functions
125 |       templater.functions_generator.generate_object = async function (
126 |         config,
127 |         functions_mode,
128 |       ) {
129 |         const functions = await oldGenerateObject(config, functions_mode);
130 |         Object.assign(functions, { mcpTools: { prompt } });
131 |         return functions;
132 |       };
133 | 
134 |       // Process template with variables
135 |       const processedContent = await templater.read_and_parse_template(config);
136 | 
137 |       // Restore original functions generator
138 |       templater.functions_generator.generate_object = oldGenerateObject;
139 | 
140 |       // Create new file if requested
141 |       if (params.createFile && params.targetPath) {
142 |         await this.app.vault.create(params.targetPath, processedContent);
143 |         res.json({
144 |           message: "Prompt executed and file created successfully",
145 |           content: processedContent,
146 |         });
147 |         return;
148 |       }
149 | 
150 |       res.json({
151 |         message: "Prompt executed without creating a file",
152 |         content: processedContent,
153 |       });
154 |     } catch (error) {
155 |       logger.error("Prompt execution error:", {
156 |         error: error instanceof Error ? error.message : error,
157 |         body: req.body,
158 |       });
159 |       res.status(503).json({
160 |         error: "An error occurred while processing the prompt",
161 |       });
162 |       return;
163 |     }
164 |   }
165 | 
166 |   private async handleSearchRequest(req: Request, res: Response) {
167 |     try {
168 |       const dep = await lastValueFrom(loadSmartSearchAPI(this));
169 |       const smartSearch = dep.api;
170 |       if (!smartSearch) {
171 |         new Notice(
172 |           "Smart Search REST API Plugin: smart-connections plugin is required but not found. Please install it from the community plugins.",
173 |           0,
174 |         );
175 |         res.status(503).json({
176 |           error: "Smart Connections plugin is not available",
177 |         });
178 |         return;
179 |       }
180 | 
181 |       // Validate request body
182 |       const requestBody = jsonSearchRequest
183 |         .pipe(({ query, filter = {} }) => ({
184 |           query,
185 |           filter: shake({
186 |             key_starts_with_any: filter.folders,
187 |             exclude_key_starts_with_any: filter.excludeFolders,
188 |             limit: filter.limit,
189 |           }),
190 |         }))
191 |         .to(searchParameters)(req.body);
192 |       if (requestBody instanceof type.errors) {
193 |         res.status(400).json({
194 |           error: "Invalid request body",
195 |           summary: requestBody.summary,
196 |         });
197 |         return;
198 |       }
199 | 
200 |       // Perform search
201 |       const results = await smartSearch.search(
202 |         requestBody.query,
203 |         requestBody.filter,
204 |       );
205 | 
206 |       // Format response
207 |       const response: SearchResponse = {
208 |         results: await Promise.all(
209 |           results.map(async (result) => ({
210 |             path: result.item.path,
211 |             text: await result.item.read(),
212 |             score: result.score,
213 |             breadcrumbs: result.item.breadcrumbs,
214 |           })),
215 |         ),
216 |       };
217 | 
218 |       res.json(response);
219 |       return;
220 |     } catch (error) {
221 |       logger.error("Smart Search API error:", { error, body: req.body });
222 |       res.status(503).json({
223 |         error: "An error occurred while processing the search request",
224 |       });
225 |       return;
226 |     }
227 |   }
228 | 
229 |   onunload() {
230 |     this.localRestApi.api?.unregister();
231 |   }
232 | }
233 | 
```

--------------------------------------------------------------------------------
/packages/obsidian-plugin/src/shared/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { App } from "obsidian";
  2 | import { getAPI, LocalRestApiPublicApi } from "obsidian-local-rest-api";
  3 | import {
  4 |   distinct,
  5 |   interval,
  6 |   map,
  7 |   merge,
  8 |   scan,
  9 |   startWith,
 10 |   takeUntil,
 11 |   takeWhile,
 12 |   timer,
 13 | } from "rxjs";
 14 | import type { SmartConnections, Templater } from "shared";
 15 | import type McpToolsPlugin from "src/main";
 16 | 
 17 | export interface Dependency<ID extends keyof App["plugins"]["plugins"], API> {
 18 |   id: keyof Dependencies;
 19 |   name: string;
 20 |   required: boolean;
 21 |   installed: boolean;
 22 |   url?: string;
 23 |   api?: API;
 24 |   plugin?: App["plugins"]["plugins"][ID];
 25 | }
 26 | 
 27 | export interface Dependencies {
 28 |   "obsidian-local-rest-api": Dependency<
 29 |     "obsidian-local-rest-api",
 30 |     LocalRestApiPublicApi
 31 |   >;
 32 |   "smart-connections": Dependency<
 33 |     "smart-connections",
 34 |     SmartConnections.SmartSearch
 35 |   >;
 36 |   "templater-obsidian": Dependency<"templater-obsidian", Templater.ITemplater>;
 37 | }
 38 | 
 39 | // Smart Connections v3.0+ uses a Smart Environment architecture instead of window.SmartSearch
 40 | declare const window: {
 41 |   SmartSearch?: SmartConnections.SmartSearch;
 42 | } & Window;
 43 | 
 44 | export const loadSmartSearchAPI = (plugin: McpToolsPlugin) =>
 45 |   interval(200).pipe(
 46 |     takeUntil(timer(5000)),
 47 |     map((): Dependencies["smart-connections"] => {
 48 |       const smartConnectionsPlugin = plugin.app.plugins.plugins[
 49 |         "smart-connections"
 50 |       ] as any;
 51 | 
 52 |       // Check for Smart Connections v3.0+ (uses smart environment)
 53 |       if (smartConnectionsPlugin?.env?.smart_sources) {
 54 |         const smartEnv = smartConnectionsPlugin.env;
 55 | 
 56 |         // Create a compatibility wrapper that matches the old SmartSearch interface
 57 |         const api: SmartConnections.SmartSearch = {
 58 |           search: async (
 59 |             search_text: string,
 60 |             filter?: Record<string, string>,
 61 |           ) => {
 62 |             try {
 63 |               // Use the new v3.0 lookup API
 64 |               const results = await smartEnv.smart_sources.lookup({
 65 |                 hypotheticals: [search_text],
 66 |                 filter: {
 67 |                   limit: filter?.limit,
 68 |                   key_starts_with_any: filter?.key_starts_with_any,
 69 |                   exclude_key_starts_with_any:
 70 |                     filter?.exclude_key_starts_with_any,
 71 |                   exclude_key: filter?.exclude_key,
 72 |                   exclude_keys: filter?.exclude_keys,
 73 |                   exclude_key_starts_with: filter?.exclude_key_starts_with,
 74 |                   exclude_key_includes: filter?.exclude_key_includes,
 75 |                   key_ends_with: filter?.key_ends_with,
 76 |                   key_starts_with: filter?.key_starts_with,
 77 |                   key_includes: filter?.key_includes,
 78 |                 },
 79 |               });
 80 | 
 81 |               // Transform results to match expected format
 82 |               return results.map((result: any) => ({
 83 |                 item: {
 84 |                   path: result.item.path,
 85 |                   name:
 86 |                     result.item.name ||
 87 |                     result.item.key?.split("/").pop() ||
 88 |                     result.item.key,
 89 |                   breadcrumbs: result.item.breadcrumbs || result.item.path,
 90 |                   read: () => result.item.read(),
 91 |                   key: result.item.key,
 92 |                   file_path: result.item.path,
 93 |                   link: result.item.link,
 94 |                   size: result.item.size,
 95 |                 },
 96 |                 score: result.score,
 97 |               }));
 98 |             } catch (error) {
 99 |               console.error("Smart Connections v3.0 search error:", error);
100 |               return [];
101 |             }
102 |           },
103 |         };
104 | 
105 |         return {
106 |           id: "smart-connections",
107 |           name: "Smart Connections",
108 |           required: false,
109 |           installed: true,
110 |           api,
111 |           plugin: smartConnectionsPlugin,
112 |         };
113 |       }
114 | 
115 |       // Try window.SmartSearch first (works on some platforms for v2.x)
116 |       let legacyApi = window.SmartSearch;
117 | 
118 |       // Fallback to plugin system (fixes Linux/cross-platform detection issues)
119 |       if (!legacyApi && smartConnectionsPlugin?.env) {
120 |         legacyApi = smartConnectionsPlugin.env;
121 |         // Cache it for future use
122 |         window.SmartSearch = legacyApi;
123 |       }
124 | 
125 |       return {
126 |         id: "smart-connections",
127 |         name: "Smart Connections",
128 |         required: false,
129 |         installed: !!legacyApi,
130 |         api: legacyApi,
131 |         plugin: smartConnectionsPlugin,
132 |       };
133 |     }),
134 |     takeWhile((dependency) => !dependency.installed, true),
135 |     distinct(({ installed }) => installed),
136 |   );
137 | 
138 | export const loadLocalRestAPI = (plugin: McpToolsPlugin) =>
139 |   interval(200).pipe(
140 |     takeUntil(timer(5000)),
141 |     map((): Dependencies["obsidian-local-rest-api"] => {
142 |       const api = getAPI(plugin.app, plugin.manifest);
143 |       return {
144 |         id: "obsidian-local-rest-api",
145 |         name: "Local REST API",
146 |         required: true,
147 |         installed: !!api,
148 |         api,
149 |         plugin: plugin.app.plugins.plugins["obsidian-local-rest-api"],
150 |       };
151 |     }),
152 |     takeWhile((dependency) => !dependency.installed, true),
153 |     distinct(({ installed }) => installed),
154 |   );
155 | 
156 | export const loadTemplaterAPI = (plugin: McpToolsPlugin) =>
157 |   interval(200).pipe(
158 |     takeUntil(timer(5000)),
159 |     map((): Dependencies["templater-obsidian"] => {
160 |       const api = plugin.app.plugins.plugins["templater-obsidian"]?.templater;
161 |       return {
162 |         id: "templater-obsidian",
163 |         name: "Templater",
164 |         required: false,
165 |         installed: !!api,
166 |         api,
167 |         plugin: plugin.app.plugins.plugins["templater-obsidian"],
168 |       };
169 |     }),
170 |     takeWhile((dependency) => !dependency.installed, true),
171 |     distinct(({ installed }) => installed),
172 |   );
173 | 
174 | export const loadDependencies = (plugin: McpToolsPlugin) => {
175 |   const dependencies: Dependencies = {
176 |     "obsidian-local-rest-api": {
177 |       id: "obsidian-local-rest-api",
178 |       name: "Local REST API",
179 |       required: true,
180 |       installed: false,
181 |       url: "https://github.com/coddingtonbear/obsidian-local-rest-api",
182 |     },
183 |     "smart-connections": {
184 |       id: "smart-connections",
185 |       name: "Smart Connections",
186 |       required: false,
187 |       installed: false,
188 |       url: "https://smartconnections.app/",
189 |     },
190 |     "templater-obsidian": {
191 |       id: "templater-obsidian",
192 |       name: "Templater",
193 |       required: false,
194 |       installed: false,
195 |       url: "https://silentvoid13.github.io/Templater/",
196 |     },
197 |   };
198 |   return merge(
199 |     loadLocalRestAPI(plugin),
200 |     loadTemplaterAPI(plugin),
201 |     loadSmartSearchAPI(plugin),
202 |   ).pipe(
203 |     scan((acc, dependency) => {
204 |       // @ts-expect-error Dynamic key assignment
205 |       acc[dependency.id] = {
206 |         ...dependencies[dependency.id],
207 |         ...dependency,
208 |       };
209 |       return acc;
210 |     }, dependencies),
211 |     startWith(dependencies),
212 |   );
213 | };
214 | 
215 | export const loadDependenciesArray = (plugin: McpToolsPlugin) =>
216 |   loadDependencies(plugin).pipe(
217 |     map((deps) => Object.values(deps) as Dependencies[keyof Dependencies][]),
218 |   );
219 | 
220 | export * from "./logger";
221 | 
```

--------------------------------------------------------------------------------
/docs/features/mcp-server-install.md:
--------------------------------------------------------------------------------

```markdown
  1 | # MCP Server Installation Feature Requirements
  2 | 
  3 | ## Overview
  4 | 
  5 | This feature enables users to install and manage the MCP server executable through the Obsidian plugin settings interface. The system handles the download of platform-specific binaries, Claude Desktop configuration, and provides clear user feedback throughout the process.
  6 | 
  7 | ## Implementation Location
  8 | 
  9 | The installation feature is implemented in the Obsidian plugin package under `src/features/mcp-server-install`.
 10 | 
 11 | ## Installation Flow
 12 | 
 13 | 1. User Prerequisites:
 14 | 
 15 |    - Claude Desktop installed
 16 |    - Local REST API plugin installed and configured with API key
 17 |    - (Optional) Templater plugin for enhanced functionality
 18 |    - (Optional) Smart Connections plugin for enhanced search
 19 | 
 20 | 2. Installation Steps:
 21 |    - User navigates to plugin settings
 22 |    - Plugin verifies prerequisites and shows status
 23 |    - User initiates installation via button
 24 |    - Plugin retrieves API key from Local REST API plugin
 25 |    - Plugin downloads appropriate binary
 26 |    - Plugin updates Claude config file
 27 |    - Plugin confirms successful installation
 28 | 
 29 | ## Settings UI Requirements
 30 | 
 31 | The settings UI is implemented as a Svelte component in `components/SettingsTab.svelte`.
 32 | 
 33 | 1. Component Structure:
 34 |    ```svelte
 35 |    <script lang="ts">
 36 |      // Import Svelte stores for state management
 37 |      import { installationStatus } from '../stores/status';
 38 |      import { dependencies } from '../stores/dependencies';
 39 |      
 40 |      // Props from parent Settings.svelte
 41 |      export let plugin: Plugin;
 42 |    </script>
 43 | 
 44 |    <!-- Installation status and controls -->
 45 |    <div class="installation-status">
 46 |      <!-- Dynamic content based on $installationStatus -->
 47 |    </div>
 48 | 
 49 |    <!-- Dependencies section -->
 50 |    <div class="dependencies">
 51 |      <!-- Dynamic content based on $dependencies -->
 52 |    </div>
 53 | 
 54 |    <!-- Links section -->
 55 |    <div class="links">
 56 |      <!-- External resource links -->
 57 |    </div>
 58 |    ```
 59 | 
 60 | 2. Display Elements:
 61 |    - Installation status indicator with version
 62 |    - Install/Update/Uninstall buttons
 63 |    - Dependency status and links
 64 |    - Links to:
 65 |      - Downloaded executable location (with folder access)
 66 |      - Log folder location (with folder access)
 67 |      - GitHub repository
 68 |      - Claude Desktop download page (when needed)
 69 |      - Required and recommended plugins
 70 | 
 71 | 3. State Management:
 72 |    - Uses Svelte stores for reactive state
 73 |    - Status states:
 74 |      - Not Installed
 75 |      - Installing
 76 |      - Installed
 77 |      - Update Available
 78 | 
 79 | ## Download Management
 80 | 
 81 | 1. Binary Source:
 82 | 
 83 |    - GitHub latest release
 84 |    - Platform-specific naming conventions
 85 |    - Version number included in filename (e.g., mcp-server-1.2.3)
 86 | 
 87 | 2. Installation Locations:
 88 |    - Binary: {vault}/.obsidian/plugins/{plugin-id}/bin/
 89 |    - Logs:
 90 |      - macOS: ~/Library/Logs/obsidian-mcp-tools
 91 |      - Windows: %APPDATA%\obsidian-mcp-tools\logs
 92 |      - Linux: (platform-specific path)
 93 | 
 94 | ## Claude Configuration
 95 | 
 96 | 1. Config File:
 97 |    - Location: ~/Library/Application Support/Claude/claude_desktop_config.json
 98 |    - Create base structure if missing: { "mcpServers": {} }
 99 |    - Add/update only our config entry:
100 |      ```json
101 |      {
102 |        "mcpServers": {
103 |          "obsidian-mcp-tools": {
104 |            "command": "(absolute path to executable)",
105 |            "env": {
106 |              "OBSIDIAN_API_KEY": "(stored api key)"
107 |            }
108 |          }
109 |        }
110 |      }
111 |      ```
112 | 
113 | ## Version Management
114 | 
115 | 1. Unified Version Approach:
116 |    - Plugin and server share same version number
117 |    - Version stored in plugin manifest
118 |    - Server provides version via `--version` flag
119 |    - Version checked during plugin initialization
120 | 
121 | ## User Education
122 | 
123 | 1. Documentation Requirements:
124 |    - README.md must explain:
125 |      - Binary download and installation process
126 |      - GitHub source code location
127 |      - Claude config file modifications
128 |      - Log file locations and purpose
129 |    - Settings page must link to full documentation
130 | 
131 | ## Error Handling
132 | 
133 | 1. Installation Errors:
134 | 
135 |    - Claude Desktop not installed
136 |    - Download failures
137 |    - Permission issues
138 |    - Version mismatch
139 | 
140 | 2. User Feedback:
141 |    - Use Obsidian Notice API for progress/status
142 |    - Clear error messages with next steps
143 |    - Links to troubleshooting resources
144 | 
145 | ## Uninstall Process
146 | 
147 | 1. Cleanup Actions:
148 |    - Remove executable
149 |    - Remove our entry from Claude config
150 |    - Clear stored plugin data
151 | 
152 | ## Appendix: Implementation Insights
153 | 
154 | ### Feature Organization
155 | The feature follows a modular structure:
156 | ```
157 | src/features/mcp-server-install/
158 | ├── components/       # Svelte components
159 | │   └── SettingsTab.svelte
160 | ├── services/        # Core functionality
161 | │   ├── config.ts    # Claude config management
162 | │   ├── download.ts  # Binary download
163 | │   ├── status.ts    # Installation status
164 | │   └── uninstall.ts # Cleanup operations
165 | ├── stores/          # Svelte stores
166 | │   ├── status.ts    # Installation status store
167 | │   └── dependencies.ts # Dependencies status store
168 | ├── utils/           # Shared utilities
169 | │   └── openFolder.ts
170 | ├── constants.ts     # Configuration
171 | ├── types.ts         # Type definitions
172 | └── index.ts         # Feature setup & component export
173 | ```
174 | 
175 | ### Key Implementation Decisions
176 | 
177 | 1. API Key Management
178 |    - Removed manual API key input
179 |    - Automatically retrieved from Local REST API plugin
180 |    - Reduces user friction and potential errors
181 | 
182 | 2. Symlink Resolution
183 |    - Added robust symlink handling for binary paths
184 |    - Ensures correct operation even with complex vault setups
185 |    - Handles non-existent paths during resolution
186 | 
187 | 3. Status Management
188 |    - Unified status interface with version tracking
189 |    - Real-time status updates during operations
190 |    - Clear feedback for update availability
191 | 
192 | 4. Error Handling
193 |    - Comprehensive prerequisite validation
194 |    - Detailed error messages with next steps
195 |    - Proper cleanup on failures
196 |    - Extensive logging for troubleshooting
197 | 
198 | 5. User Experience
199 |    - Reactive UI with Svelte components
200 |    - One-click installation process
201 |    - Direct access to logs and binaries
202 |    - Clear dependency requirements
203 |    - Links to all required and recommended plugins
204 |    - Real-time status updates through Svelte stores
205 | 
206 | ### Recommended Plugins
207 | Added information about recommended plugins that enhance functionality:
208 | - Templater: For template-based operations
209 | - Smart Connections: For enhanced search capabilities
210 | - Local REST API: Required for Obsidian communication
211 | 
212 | ### Platform Compatibility
213 | Implemented robust platform detection and path handling:
214 | - Windows: Handles UNC paths and environment variables
215 | - macOS: Proper binary permissions and config paths
216 | - Linux: Flexible configuration for various distributions
217 | 
218 | ### Future Considerations
219 | 1. Version Management
220 |    - Consider automated update checks
221 |    - Add update notifications
222 |    - Implement rollback capability
223 | 
224 | 2. Configuration
225 |    - Add backup/restore of Claude config
226 |    - Support custom binary locations
227 |    - Allow custom log paths
228 | 
229 | 3. Error Recovery
230 |    - Add self-repair functionality
231 |    - Implement health checks
232 |    - Add diagnostic tools
233 | 
```

--------------------------------------------------------------------------------
/packages/obsidian-plugin/src/features/mcp-server-install/services/install.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import fs from "fs";
  2 | import fsp from "fs/promises";
  3 | import https from "https";
  4 | import { Notice, Plugin } from "obsidian";
  5 | import os from "os";
  6 | import { Observable } from "rxjs";
  7 | import { logger } from "$/shared";
  8 | import { GITHUB_DOWNLOAD_URL, type Arch, type Platform } from "../constants";
  9 | import type { DownloadProgress, InstallPathInfo } from "../types";
 10 | import { getInstallPath } from "./status";
 11 | 
 12 | export function getPlatform(): Platform {
 13 |   const platform = os.platform();
 14 |   switch (platform) {
 15 |     case "darwin":
 16 |       return "macos";
 17 |     case "win32":
 18 |       return "windows";
 19 |     default:
 20 |       return "linux";
 21 |   }
 22 | }
 23 | 
 24 | export function getArch(): Arch {
 25 |   return os.arch() as Arch;
 26 | }
 27 | 
 28 | export function getDownloadUrl(platform: Platform, arch: Arch): string {
 29 |   if (platform === "windows") {
 30 |     return `${GITHUB_DOWNLOAD_URL}/mcp-server-windows.exe`;
 31 |   } else if (platform === "macos") {
 32 |     return `${GITHUB_DOWNLOAD_URL}/mcp-server-macos-${arch}`;
 33 |   } else { // linux
 34 |     return `${GITHUB_DOWNLOAD_URL}/mcp-server-linux`;  // Linux binary doesn't include arch in filename
 35 |   }
 36 | }
 37 | 
 38 | /**
 39 |  * Ensures that the specified directory path exists and is writable.
 40 |  *
 41 |  * If the directory does not exist, it will be created recursively. If the directory
 42 |  * exists but is not writable, an error will be thrown.
 43 |  *
 44 |  * @param dirpath - The real directory path to ensure exists and is writable.
 45 |  * @throws {Error} If the directory does not exist or is not writable.
 46 |  */
 47 | export async function ensureDirectory(dirpath: string) {
 48 |   try {
 49 |     if (!fs.existsSync(dirpath)) {
 50 |       await fsp.mkdir(dirpath, { recursive: true });
 51 |     }
 52 | 
 53 |     // Verify directory was created and is writable
 54 |     try {
 55 |       await fsp.access(dirpath, fs.constants.W_OK);
 56 |     } catch (accessError) {
 57 |       throw new Error(`Directory exists but is not writable: ${dirpath}`);
 58 |     }
 59 |   } catch (error) {
 60 |     logger.error(`Failed to ensure directory:`, { error });
 61 |     throw error;
 62 |   }
 63 | }
 64 | 
 65 | export function downloadFile(
 66 |   url: string,
 67 |   outputPath: string,
 68 |   redirects = 0,
 69 | ): Observable<DownloadProgress> {
 70 |   return new Observable((subscriber) => {
 71 |     if (redirects > 5) {
 72 |       subscriber.error(new Error("Too many redirects"));
 73 |       return;
 74 |     }
 75 | 
 76 |     let fileStream: fs.WriteStream | undefined;
 77 |     const cleanup = (err?: unknown) => {
 78 |       if (err) {
 79 |         logger.debug("Cleaning up incomplete download:", {
 80 |           outputPath,
 81 |           writableFinished: JSON.stringify(fileStream?.writableFinished),
 82 |           error: err instanceof Error ? err.message : String(err),
 83 |         });
 84 |         fileStream?.destroy();
 85 |         fsp.unlink(outputPath).catch((unlinkError) => {
 86 |           logger.error("Failed to clean up incomplete download:", {
 87 |             outputPath,
 88 |             error:
 89 |               unlinkError instanceof Error
 90 |                 ? unlinkError.message
 91 |                 : String(unlinkError),
 92 |           });
 93 |         });
 94 |       } else {
 95 |         fileStream?.close();
 96 |         fsp.chmod(outputPath, 0o755).catch((chmodError) => {
 97 |           logger.error("Failed to set executable permissions:", {
 98 |             outputPath,
 99 |             error:
100 |               chmodError instanceof Error
101 |                 ? chmodError.message
102 |                 : String(chmodError),
103 |           });
104 |         });
105 |       }
106 |     };
107 | 
108 |     https
109 |       .get(url, (response) => {
110 |         try {
111 |           if (!response) {
112 |             throw new Error("No response received");
113 |           }
114 | 
115 |           const statusCode = response.statusCode ?? 0;
116 | 
117 |           // Handle various HTTP status codes
118 |           if (statusCode >= 400) {
119 |             throw new Error(
120 |               `HTTP Error ${statusCode}: ${response.statusMessage}`,
121 |             );
122 |           }
123 | 
124 |           if (statusCode === 302 || statusCode === 301) {
125 |             const redirectUrl = response.headers.location;
126 |             if (!redirectUrl) {
127 |               throw new Error(
128 |                 `Redirect (${statusCode}) received but no location header found`,
129 |               );
130 |             }
131 | 
132 |             // Handle redirect by creating a new observable
133 |             downloadFile(redirectUrl, outputPath, redirects + 1).subscribe(
134 |               subscriber,
135 |             );
136 |             return;
137 |           }
138 | 
139 |           if (statusCode !== 200) {
140 |             throw new Error(`Unexpected status code: ${statusCode}`);
141 |           }
142 | 
143 |           // Validate content length
144 |           const contentLength = response.headers["content-length"];
145 |           const totalBytes = contentLength ? parseInt(contentLength, 10) : 0;
146 |           if (contentLength && isNaN(totalBytes)) {
147 |             throw new Error("Invalid content-length header");
148 |           }
149 | 
150 |           try {
151 |             fileStream = fs.createWriteStream(outputPath, {
152 |               flags: "w",
153 |             });
154 |           } catch (err) {
155 |             throw new Error(
156 |               `Failed to create write stream: ${err instanceof Error ? err.message : String(err)}`,
157 |             );
158 |           }
159 | 
160 |           let downloadedBytes = 0;
161 | 
162 |           fileStream.on("error", (err) => {
163 |             const fileStreamError = new Error(
164 |               `File stream error: ${err.message}`,
165 |             );
166 |             cleanup(fileStreamError);
167 |             subscriber.error(fileStreamError);
168 |           });
169 | 
170 |           response.on("data", (chunk: Buffer) => {
171 |             try {
172 |               if (!Buffer.isBuffer(chunk)) {
173 |                 throw new Error("Received invalid data chunk");
174 |               }
175 | 
176 |               downloadedBytes += chunk.length;
177 |               const percentage = totalBytes
178 |                 ? (downloadedBytes / totalBytes) * 100
179 |                 : 0;
180 | 
181 |               subscriber.next({
182 |                 bytesReceived: downloadedBytes,
183 |                 totalBytes,
184 |                 percentage: Math.round(percentage * 100) / 100,
185 |               });
186 |             } catch (err) {
187 |               cleanup(err);
188 |               subscriber.error(err);
189 |             }
190 |           });
191 | 
192 |           response.pipe(fileStream);
193 | 
194 |           fileStream.on("finish", () => {
195 |             cleanup();
196 |             subscriber.complete();
197 |           });
198 | 
199 |           response.on("error", (err) => {
200 |             cleanup(err);
201 |             subscriber.error(new Error(`Response error: ${err.message}`));
202 |           });
203 |         } catch (err) {
204 |           cleanup(err);
205 |           subscriber.error(err instanceof Error ? err : new Error(String(err)));
206 |         }
207 |       })
208 |       .on("error", (err) => {
209 |         cleanup(err);
210 |         subscriber.error(new Error(`Network error: ${err.message}`));
211 |       });
212 |   });
213 | }
214 | 
215 | export async function installMcpServer(
216 |   plugin: Plugin,
217 | ): Promise<InstallPathInfo> {
218 |   try {
219 |     const platform = getPlatform();
220 |     const arch = getArch();
221 |     const downloadUrl = getDownloadUrl(platform, arch);
222 |     const installPath = await getInstallPath(plugin);
223 |     if ("error" in installPath) throw new Error(installPath.error);
224 | 
225 |     await ensureDirectory(installPath.dir);
226 | 
227 |     const progressNotice = new Notice("Downloading MCP server...", 0);
228 |     logger.debug("Downloading MCP server:", { downloadUrl, installPath });
229 | 
230 |     const download$ = downloadFile(downloadUrl, installPath.path);
231 | 
232 |     return new Promise((resolve, reject) => {
233 |       download$.subscribe({
234 |         next: (progress: DownloadProgress) => {
235 |           progressNotice.setMessage(
236 |             `Downloading MCP server: ${progress.percentage}%`,
237 |           );
238 |         },
239 |         error: (error: Error) => {
240 |           progressNotice.hide();
241 |           new Notice(`Failed to download MCP server: ${error.message}`);
242 |           logger.error("Download failed:", { error, installPath });
243 |           reject(error);
244 |         },
245 |         complete: () => {
246 |           progressNotice.hide();
247 |           new Notice("MCP server downloaded successfully!");
248 |           logger.info("MCP server downloaded", { installPath });
249 |           resolve(installPath);
250 |         },
251 |       });
252 |     });
253 |   } catch (error) {
254 |     new Notice(
255 |       `Failed to install MCP server: ${error instanceof Error ? error.message : String(error)}`,
256 |     );
257 |     throw error;
258 |   }
259 | }
260 | 
```

--------------------------------------------------------------------------------
/packages/shared/src/types/plugin-local-rest-api.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { type } from "arktype";
  2 | 
  3 | /**
  4 |  * Error response from the API
  5 |  * Content-Type: application/json
  6 |  * Used in various error responses across endpoints
  7 |  * @property errorCode - A 5-digit error code uniquely identifying this particular type of error
  8 |  * @property message - Message describing the error
  9 |  */
 10 | export const ApiError = type({
 11 |   errorCode: "number",
 12 |   message: "string",
 13 | });
 14 | 
 15 | /**
 16 |  * JSON representation of a note including parsed tag and frontmatter data as well as filesystem metadata
 17 |  * Content-Type: application/vnd.olrapi.note+json
 18 |  * GET /vault/{filename} or GET /active/ with Accept: application/vnd.olrapi.note+json
 19 |  */
 20 | export const ApiNoteJson = type({
 21 |   content: "string",
 22 |   frontmatter: "Record<string, string>",
 23 |   path: "string",
 24 |   stat: {
 25 |     ctime: "number",
 26 |     mtime: "number",
 27 |     size: "number",
 28 |   },
 29 |   tags: "string[]",
 30 | });
 31 | 
 32 | /**
 33 |  * Defines the structure of a plugin manifest, which contains metadata about a plugin.
 34 |  * This type is used to represent the response from the API's root endpoint, providing
 35 |  * basic server details and authentication status.
 36 |  */
 37 | const ApiPluginManifest = type({
 38 |   id: "string",
 39 |   name: "string",
 40 |   version: "string",
 41 |   minAppVersion: "string",
 42 |   description: "string",
 43 |   author: "string",
 44 |   authorUrl: "string",
 45 |   isDesktopOnly: "boolean",
 46 |   dir: "string",
 47 | });
 48 | 
 49 | /**
 50 |  * Response from the root endpoint providing basic server details and authentication status
 51 |  * Content-Type: application/json
 52 |  * GET / - This is the only API request that does not require authentication
 53 |  */
 54 | export const ApiStatusResponse = type({
 55 |   status: "string",
 56 |   manifest: ApiPluginManifest,
 57 |   versions: {
 58 |     obsidian: "string",
 59 |     self: "string",
 60 |   },
 61 |   service: "string",
 62 |   authenticated: "boolean",
 63 |   certificateInfo: {
 64 |     validityDays: "number",
 65 |     regenerateRecommended: "boolean",
 66 |   },
 67 |   apiExtensions: ApiPluginManifest.array(),
 68 | });
 69 | 
 70 | /**
 71 |  * Response from searching vault files using advanced search
 72 |  * Content-Type: application/json
 73 |  * POST /search/
 74 |  * Returns array of matching files and their results
 75 |  * Results are only returned for non-falsy matches
 76 |  */
 77 | export const ApiSearchResponse = type({
 78 |   filename: "string",
 79 |   result: "string|number|string[]|object|boolean",
 80 | }).array();
 81 | 
 82 | /**
 83 |  * Match details for simple text search results
 84 |  * Content-Type: application/json
 85 |  * Used in ApiSimpleSearchResult
 86 |  */
 87 | export const ApiSimpleSearchMatch = type({
 88 |   match: {
 89 |     start: "number",
 90 |     end: "number",
 91 |   },
 92 |   context: "string",
 93 | });
 94 | 
 95 | /**
 96 |  * Result from searching vault files with simple text search
 97 |  * Content-Type: application/json
 98 |  * POST /search/simple/
 99 |  * Returns matches with surrounding context
100 |  */
101 | export const ApiSimpleSearchResponse = type({
102 |   filename: "string",
103 |   matches: ApiSimpleSearchMatch.array(),
104 |   score: "number",
105 | }).array();
106 | 
107 | /**
108 |  * Result entry from semantic search
109 |  * Content-Type: application/json
110 |  * Used in ApiSearchResponse
111 |  */
112 | export const ApiSmartSearchResult = type({
113 |   path: "string",
114 |   text: "string",
115 |   score: "number",
116 |   breadcrumbs: "string",
117 | });
118 | 
119 | /**
120 |  * Response from semantic search containing list of matching results
121 |  * Content-Type: application/json
122 |  * POST /search/smart/
123 |  */
124 | export const ApiSmartSearchResponse = type({
125 |   results: ApiSmartSearchResult.array(),
126 | });
127 | 
128 | /**
129 |  * Parameters for semantic search request
130 |  * Content-Type: application/json
131 |  * POST /search/smart/
132 |  * @property query - A search phrase for semantic search
133 |  * @property filter.folders - An array of folder names to include. For example, ["Public", "Work"]
134 |  * @property filter.excludeFolders - An array of folder names to exclude. For example, ["Private", "Archive"]
135 |  * @property filter.limit - The maximum number of results to return
136 |  */
137 | export const ApiSearchParameters = type({
138 |   query: "string",
139 |   filter: {
140 |     folders: "string[]?",
141 |     excludeFolders: "string[]?",
142 |     limit: "number?",
143 |   },
144 | });
145 | 
146 | /**
147 |  * Command information from Obsidian's command palette
148 |  * Content-Type: application/json
149 |  * Used in ApiCommandsResponse
150 |  */
151 | export const ApiCommand = type({
152 |   id: "string",
153 |   name: "string",
154 | });
155 | 
156 | /**
157 |  * Response containing list of available Obsidian commands
158 |  * Content-Type: application/json
159 |  * GET /commands/
160 |  */
161 | export const ApiCommandsResponse = type({
162 |   commands: ApiCommand.array(),
163 | });
164 | 
165 | /**
166 |  * Response containing list of files in a vault directory
167 |  * Content-Type: application/json
168 |  * GET /vault/ or GET /vault/{pathToDirectory}/
169 |  * Note that empty directories will not be returned
170 |  */
171 | export const ApiVaultDirectoryResponse = type({
172 |   files: "string[]",
173 | });
174 | 
175 | /**
176 |  * Response containing vault file information
177 |  * Content-Type: application/json
178 |  * POST /vault/{pathToFile}
179 |  * Returns array of matching files and their results
180 |  * Results are only returned for non-falsy matches
181 |  */
182 | export const ApiVaultFileResponse = type({
183 |   frontmatter: {
184 |     tags: "string[]",
185 |     description: "string?",
186 |   },
187 |   content: "string",
188 |   path: "string",
189 |   stat: {
190 |     ctime: "number",
191 |     mtime: "number",
192 |     size: "number",
193 |   },
194 |   tags: "string[]",
195 | });
196 | 
197 | /**
198 |  * Parameters for patching a file or document in the Obsidian plugin's REST API.
199 |  * This type defines the expected request body for the patch operation.
200 |  *
201 |  * @property operation - Specifies how to modify the content: append (add after), prepend (add before), or replace existing content
202 |  * @property targetType - Identifies what to modify: a section under a heading, a referenced block, or a frontmatter field
203 |  * @property target - The identifier - either heading path (e.g. 'Heading 1::Subheading 1:1'), block reference ID, or frontmatter field name
204 |  * @property targetDelimiter - The separator used in heading paths to indicate nesting (default '::')
205 |  * @property trimTargetWhitespace - Whether to remove whitespace from target identifier before matching (default: false)
206 |  * @property content - The actual content to insert, append, or use as replacement
207 |  * @property contentType - Format of the content - use application/json for structured data like table rows or frontmatter values
208 |  */
209 | export const ApiPatchParameters = type({
210 |   operation: type("'append' | 'prepend' | 'replace'").describe(
211 |     "Specifies how to modify the content: append (add after), prepend (add before), or replace existing content",
212 |   ),
213 |   targetType: type("'heading' | 'block' | 'frontmatter'").describe(
214 |     "Identifies what to modify: a section under a heading, a referenced block, or a frontmatter field",
215 |   ),
216 |   target: type("string").describe(
217 |     "The identifier - either heading path (e.g. 'Heading 1::Subheading 1:1'), block reference ID, or frontmatter field name",
218 |   ),
219 |   "targetDelimiter?": type("string").describe(
220 |     "The separator used in heading paths to indicate nesting (default '::')",
221 |   ),
222 |   "trimTargetWhitespace?": type("boolean").describe(
223 |     "Whether to remove whitespace from target identifier before matching (default: false)",
224 |   ),
225 |   content: type("string").describe(
226 |     "The actual content to insert, append, or use as replacement",
227 |   ),
228 |   "contentType?": type("'text/markdown' | 'application/json'").describe(
229 |     "Format of the content - use application/json for structured data like table rows or frontmatter values",
230 |   ),
231 | });
232 | 
233 | /**
234 |  * Represents a response containing markdown content
235 |  */
236 | export const ApiContentResponse = type("string").describe("Content");
237 | 
238 | /**
239 |  * Empty response for successful operations that don't return content
240 |  * Content-Type: none (204 No Content)
241 |  * Used by:
242 |  * - PUT /vault/{filename}
243 |  * - PUT /active/
244 |  * - PUT /periodic/{period}/
245 |  * - POST /commands/{commandId}/
246 |  * - DELETE endpoints
247 |  * Returns 204 No Content
248 |  */
249 | export const ApiNoContentResponse = type("unknown").describe("No Content");
250 | 
251 | /**
252 |  * Parameters for executing a template
253 |  * Content-Type: application/json
254 |  * POST /templates/execute/
255 |  * @property name - The name of the template to execute
256 |  * @property arguments - A key-value object of arguments to pass to the template
257 |  * @property createFile - Whether to create a new file from the template
258 |  * @property targetPath - The path to save the file; required if createFile is true
259 |  */
260 | export const ApiTemplateExecutionParams = type({
261 |   name: type("string").describe("The full vault path to the template file"),
262 |   arguments: "Record<string, string>",
263 |   "createFile?": type("boolean").describe(
264 |     "Whether to create a new file from the template",
265 |   ),
266 |   "targetPath?": type("string").describe(
267 |     "Path to save the file; required if createFile is true",
268 |   ),
269 | });
270 | 
271 | /**
272 |  * Response from executing a template
273 |  * Content-Type: application/json
274 |  * POST /templates/execute/
275 |  * @property message - A message describing the result of the template execution
276 |  */
277 | export const ApiTemplateExecutionResponse = type({
278 |   message: "string",
279 |   content: "string",
280 | });
281 | 
282 | // Export types for TypeScript usage
283 | export type ApiErrorType = typeof ApiError.infer;
284 | export type ApiNoteJsonType = typeof ApiNoteJson.infer;
285 | export type ApiStatusResponseType = typeof ApiStatusResponse.infer;
286 | export type ApiSearchResponseType = typeof ApiSearchResponse.infer;
287 | export type ApiSimpleSearchResponseType = typeof ApiSimpleSearchResponse.infer;
288 | export type ApiSmartSearchResultType = typeof ApiSmartSearchResult.infer;
289 | export type ApiSmartSearchResponseType = typeof ApiSmartSearchResponse.infer;
290 | export type ApiCommandType = typeof ApiCommand.infer;
291 | export type ApiCommandsResponseType = typeof ApiCommandsResponse.infer;
292 | export type ApiVaultDirectoryResponseType =
293 |   typeof ApiVaultDirectoryResponse.infer;
294 | export type ApiVaultFileResponseType = typeof ApiVaultFileResponse.infer;
295 | export type ApiSearchParametersType = typeof ApiSearchParameters.infer;
296 | export type ApiNoContentResponseType = typeof ApiNoContentResponse.infer;
297 | export type ApiTemplateExecutionParamsType =
298 |   typeof ApiTemplateExecutionParams.infer;
299 | export type ApiTemplateExecutionResponseType =
300 |   typeof ApiTemplateExecutionResponse.infer;
301 | 
302 | // Additional API response types can be added here
303 | export const MIME_TYPE_OLRAPI_NOTE_JSON = "application/vnd.olrapi.note+json";
304 | 
```

--------------------------------------------------------------------------------
/packages/mcp-server/src/features/local-rest-api/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { makeRequest, type ToolRegistry } from "$/shared";
  2 | import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
  3 | import { type } from "arktype";
  4 | import { LocalRestAPI } from "shared";
  5 | 
  6 | export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) {
  7 |   // GET Status
  8 |   tools.register(
  9 |     type({
 10 |       name: '"get_server_info"',
 11 |       arguments: "Record<string, unknown>",
 12 |     }).describe(
 13 |       "Returns basic details about the Obsidian Local REST API and authentication status. This is the only API request that does not require authentication.",
 14 |     ),
 15 |     async () => {
 16 |       const data = await makeRequest(LocalRestAPI.ApiStatusResponse, "/");
 17 |       return {
 18 |         content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
 19 |       };
 20 |     },
 21 |   );
 22 | 
 23 |   // GET Active File
 24 |   tools.register(
 25 |     type({
 26 |       name: '"get_active_file"',
 27 |       arguments: {
 28 |         format: type('"markdown" | "json"').optional(),
 29 |       },
 30 |     }).describe(
 31 |       "Returns the content of the currently active file in Obsidian. Can return either markdown content or a JSON representation including parsed tags and frontmatter.",
 32 |     ),
 33 |     async ({ arguments: args }) => {
 34 |       const format =
 35 |         args?.format === "json"
 36 |           ? "application/vnd.olrapi.note+json"
 37 |           : "text/markdown";
 38 |       const data = await makeRequest(
 39 |         LocalRestAPI.ApiNoteJson.or("string"),
 40 |         "/active/",
 41 |         {
 42 |           headers: { Accept: format },
 43 |         },
 44 |       );
 45 |       const content =
 46 |         typeof data === "string" ? data : JSON.stringify(data, null, 2);
 47 |       return { content: [{ type: "text", text: content }] };
 48 |     },
 49 |   );
 50 | 
 51 |   // PUT Active File
 52 |   tools.register(
 53 |     type({
 54 |       name: '"update_active_file"',
 55 |       arguments: {
 56 |         content: "string",
 57 |       },
 58 |     }).describe("Update the content of the active file open in Obsidian."),
 59 |     async ({ arguments: args }) => {
 60 |       await makeRequest(LocalRestAPI.ApiNoContentResponse, "/active/", {
 61 |         method: "PUT",
 62 |         body: args.content,
 63 |       });
 64 |       return {
 65 |         content: [{ type: "text", text: "File updated successfully" }],
 66 |       };
 67 |     },
 68 |   );
 69 | 
 70 |   // POST Active File
 71 |   tools.register(
 72 |     type({
 73 |       name: '"append_to_active_file"',
 74 |       arguments: {
 75 |         content: "string",
 76 |       },
 77 |     }).describe("Append content to the end of the currently-open note."),
 78 |     async ({ arguments: args }) => {
 79 |       await makeRequest(LocalRestAPI.ApiNoContentResponse, "/active/", {
 80 |         method: "POST",
 81 |         body: args.content,
 82 |       });
 83 |       return {
 84 |         content: [{ type: "text", text: "Content appended successfully" }],
 85 |       };
 86 |     },
 87 |   );
 88 | 
 89 |   // PATCH Active File
 90 |   tools.register(
 91 |     type({
 92 |       name: '"patch_active_file"',
 93 |       arguments: LocalRestAPI.ApiPatchParameters,
 94 |     }).describe(
 95 |       "Insert or modify content in the currently-open note relative to a heading, block reference, or frontmatter field.",
 96 |     ),
 97 |     async ({ arguments: args }) => {
 98 |       const headers: Record<string, string> = {
 99 |         Operation: args.operation,
100 |         "Target-Type": args.targetType,
101 |         Target: args.target,
102 |         "Create-Target-If-Missing": "true",
103 |       };
104 | 
105 |       if (args.targetDelimiter) {
106 |         headers["Target-Delimiter"] = args.targetDelimiter;
107 |       }
108 |       if (args.trimTargetWhitespace !== undefined) {
109 |         headers["Trim-Target-Whitespace"] = String(args.trimTargetWhitespace);
110 |       }
111 |       if (args.contentType) {
112 |         headers["Content-Type"] = args.contentType;
113 |       }
114 | 
115 |       const response = await makeRequest(
116 |         LocalRestAPI.ApiContentResponse,
117 |         "/active/",
118 |         {
119 |           method: "PATCH",
120 |           headers,
121 |           body: args.content,
122 |         },
123 |       );
124 |       return {
125 |         content: [
126 |           { type: "text", text: "File patched successfully" },
127 |           { type: "text", text: response },
128 |         ],
129 |       };
130 |     },
131 |   );
132 | 
133 |   // DELETE Active File
134 |   tools.register(
135 |     type({
136 |       name: '"delete_active_file"',
137 |       arguments: "Record<string, unknown>",
138 |     }).describe("Delete the currently-active file in Obsidian."),
139 |     async () => {
140 |       await makeRequest(LocalRestAPI.ApiNoContentResponse, "/active/", {
141 |         method: "DELETE",
142 |       });
143 |       return {
144 |         content: [{ type: "text", text: "File deleted successfully" }],
145 |       };
146 |     },
147 |   );
148 | 
149 |   // POST Open File in Obsidian UI
150 |   tools.register(
151 |     type({
152 |       name: '"show_file_in_obsidian"',
153 |       arguments: {
154 |         filename: "string",
155 |         "newLeaf?": "boolean",
156 |       },
157 |     }).describe(
158 |       "Open a document in the Obsidian UI. Creates a new document if it doesn't exist. Returns a confirmation if the file was opened successfully.",
159 |     ),
160 |     async ({ arguments: args }) => {
161 |       const query = args.newLeaf ? "?newLeaf=true" : "";
162 | 
163 |       await makeRequest(
164 |         LocalRestAPI.ApiNoContentResponse,
165 |         `/open/${encodeURIComponent(args.filename)}${query}`,
166 |         {
167 |           method: "POST",
168 |         },
169 |       );
170 | 
171 |       return {
172 |         content: [{ type: "text", text: "File opened successfully" }],
173 |       };
174 |     },
175 |   );
176 | 
177 |   // POST Search via Dataview or JsonLogic
178 |   tools.register(
179 |     type({
180 |       name: '"search_vault"',
181 |       arguments: {
182 |         queryType: '"dataview" | "jsonlogic"',
183 |         query: "string",
184 |       },
185 |     }).describe(
186 |       "Search for documents matching a specified query using either Dataview DQL or JsonLogic.",
187 |     ),
188 |     async ({ arguments: args }) => {
189 |       const contentType =
190 |         args.queryType === "dataview"
191 |           ? "application/vnd.olrapi.dataview.dql+txt"
192 |           : "application/vnd.olrapi.jsonlogic+json";
193 | 
194 |       const data = await makeRequest(
195 |         LocalRestAPI.ApiSearchResponse,
196 |         "/search/",
197 |         {
198 |           method: "POST",
199 |           headers: { "Content-Type": contentType },
200 |           body: args.query,
201 |         },
202 |       );
203 | 
204 |       return {
205 |         content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
206 |       };
207 |     },
208 |   );
209 | 
210 |   // POST Simple Search
211 |   tools.register(
212 |     type({
213 |       name: '"search_vault_simple"',
214 |       arguments: {
215 |         query: "string",
216 |         "contextLength?": "number",
217 |       },
218 |     }).describe("Search for documents matching a text query."),
219 |     async ({ arguments: args }) => {
220 |       const query = new URLSearchParams({
221 |         query: args.query,
222 |         ...(args.contextLength
223 |           ? {
224 |               contextLength: String(args.contextLength),
225 |             }
226 |           : {}),
227 |       });
228 | 
229 |       const data = await makeRequest(
230 |         LocalRestAPI.ApiSimpleSearchResponse,
231 |         `/search/simple/?${query}`,
232 |         {
233 |           method: "POST",
234 |         },
235 |       );
236 | 
237 |       return {
238 |         content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
239 |       };
240 |     },
241 |   );
242 | 
243 |   // GET Vault Files or Directories List
244 |   tools.register(
245 |     type({
246 |       name: '"list_vault_files"',
247 |       arguments: {
248 |         "directory?": "string",
249 |       },
250 |     }).describe(
251 |       "List files in the root directory or a specified subdirectory of your vault.",
252 |     ),
253 |     async ({ arguments: args }) => {
254 |       const path = args.directory ? `${args.directory}/` : "";
255 |       const data = await makeRequest(
256 |         LocalRestAPI.ApiVaultFileResponse.or(
257 |           LocalRestAPI.ApiVaultDirectoryResponse,
258 |         ),
259 |         `/vault/${path}`,
260 |       );
261 |       return {
262 |         content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
263 |       };
264 |     },
265 |   );
266 | 
267 |   // GET Vault File Content
268 |   tools.register(
269 |     type({
270 |       name: '"get_vault_file"',
271 |       arguments: {
272 |         filename: "string",
273 |         "format?": '"markdown" | "json"',
274 |       },
275 |     }).describe("Get the content of a file from your vault."),
276 |     async ({ arguments: args }) => {
277 |       const isJson = args.format === "json";
278 |       const format = isJson
279 |         ? "application/vnd.olrapi.note+json"
280 |         : "text/markdown";
281 |       const data = await makeRequest(
282 |         isJson ? LocalRestAPI.ApiNoteJson : LocalRestAPI.ApiContentResponse,
283 |         `/vault/${encodeURIComponent(args.filename)}`,
284 |         {
285 |           headers: { Accept: format },
286 |         },
287 |       );
288 |       return {
289 |         content: [
290 |           {
291 |             type: "text",
292 |             text:
293 |               typeof data === "string" ? data : JSON.stringify(data, null, 2),
294 |           },
295 |         ],
296 |       };
297 |     },
298 |   );
299 | 
300 |   // PUT Vault File Content
301 |   tools.register(
302 |     type({
303 |       name: '"create_vault_file"',
304 |       arguments: {
305 |         filename: "string",
306 |         content: "string",
307 |       },
308 |     }).describe("Create a new file in your vault or update an existing one."),
309 |     async ({ arguments: args }) => {
310 |       await makeRequest(
311 |         LocalRestAPI.ApiNoContentResponse,
312 |         `/vault/${encodeURIComponent(args.filename)}`,
313 |         {
314 |           method: "PUT",
315 |           body: args.content,
316 |         },
317 |       );
318 |       return {
319 |         content: [{ type: "text", text: "File created successfully" }],
320 |       };
321 |     },
322 |   );
323 | 
324 |   // POST Vault File Content
325 |   tools.register(
326 |     type({
327 |       name: '"append_to_vault_file"',
328 |       arguments: {
329 |         filename: "string",
330 |         content: "string",
331 |       },
332 |     }).describe("Append content to a new or existing file."),
333 |     async ({ arguments: args }) => {
334 |       await makeRequest(
335 |         LocalRestAPI.ApiNoContentResponse,
336 |         `/vault/${encodeURIComponent(args.filename)}`,
337 |         {
338 |           method: "POST",
339 |           body: args.content,
340 |         },
341 |       );
342 |       return {
343 |         content: [{ type: "text", text: "Content appended successfully" }],
344 |       };
345 |     },
346 |   );
347 | 
348 |   // PATCH Vault File Content
349 |   tools.register(
350 |     type({
351 |       name: '"patch_vault_file"',
352 |       arguments: type({
353 |         filename: "string",
354 |       }).and(LocalRestAPI.ApiPatchParameters),
355 |     }).describe(
356 |       "Insert or modify content in a file relative to a heading, block reference, or frontmatter field.",
357 |     ),
358 |     async ({ arguments: args }) => {
359 |       const headers: HeadersInit = {
360 |         Operation: args.operation,
361 |         "Target-Type": args.targetType,
362 |         Target: args.target,
363 |         "Create-Target-If-Missing": "true",
364 |       };
365 | 
366 |       if (args.targetDelimiter) {
367 |         headers["Target-Delimiter"] = args.targetDelimiter;
368 |       }
369 |       if (args.trimTargetWhitespace !== undefined) {
370 |         headers["Trim-Target-Whitespace"] = String(args.trimTargetWhitespace);
371 |       }
372 |       if (args.contentType) {
373 |         headers["Content-Type"] = args.contentType;
374 |       }
375 | 
376 |       const response = await makeRequest(
377 |         LocalRestAPI.ApiContentResponse,
378 |         `/vault/${encodeURIComponent(args.filename)}`,
379 |         {
380 |           method: "PATCH",
381 |           headers,
382 |           body: args.content,
383 |         },
384 |       );
385 | 
386 |       return {
387 |         content: [
388 |           { type: "text", text: "File patched successfully" },
389 |           { type: "text", text: response },
390 |         ],
391 |       };
392 |     },
393 |   );
394 | 
395 |   // DELETE Vault File Content
396 |   tools.register(
397 |     type({
398 |       name: '"delete_vault_file"',
399 |       arguments: {
400 |         filename: "string",
401 |       },
402 |     }).describe("Delete a file from your vault."),
403 |     async ({ arguments: args }) => {
404 |       await makeRequest(
405 |         LocalRestAPI.ApiNoContentResponse,
406 |         `/vault/${encodeURIComponent(args.filename)}`,
407 |         {
408 |           method: "DELETE",
409 |         },
410 |       );
411 |       return {
412 |         content: [{ type: "text", text: "File deleted successfully" }],
413 |       };
414 |     },
415 |   );
416 | }
417 | 
```

--------------------------------------------------------------------------------
/packages/obsidian-plugin/docs/openapi.yaml:
--------------------------------------------------------------------------------

```yaml
   1 | # Extended from Obsidian Local REST API OpenAPI Specification
   2 | # https://coddingtonbear.github.io/obsidian-local-rest-api/#/
   3 | components:
   4 |   schemas:
   5 |     Error:
   6 |       properties:
   7 |         errorCode:
   8 |           description: |
   9 |             A 5-digit error code uniquely identifying this particular type of error.
  10 |           example: 40149
  11 |           type: "number"
  12 |         message:
  13 |           description: "Message describing the error."
  14 |           example: "A brief description of the error."
  15 |           type: "string"
  16 |       type: "object"
  17 |     NoteJson:
  18 |       properties:
  19 |         content:
  20 |           type: "string"
  21 |         frontmatter:
  22 |           type: "object"
  23 |         path:
  24 |           type: "string"
  25 |         stat:
  26 |           properties:
  27 |             ctime:
  28 |               type: "number"
  29 |             mtime:
  30 |               type: "number"
  31 |             size:
  32 |               type: "number"
  33 |           required:
  34 |             - "ctime"
  35 |             - "mtime"
  36 |             - "size"
  37 |           type: "object"
  38 |         tags:
  39 |           items:
  40 |             type: "string"
  41 |           type: "array"
  42 |       required:
  43 |         - "tags"
  44 |         - "frontmatter"
  45 |         - "stat"
  46 |         - "path"
  47 |         - "content"
  48 |       type: "object"
  49 |     SearchParameters:
  50 |       type: object
  51 |       required:
  52 |         - query
  53 |       properties:
  54 |         query:
  55 |           type: string
  56 |           description: A search phrase for semantic search
  57 |           minLength: 1
  58 |         filter:
  59 |           type: object
  60 |           properties:
  61 |             folders:
  62 |               type: array
  63 |               items:
  64 |                 type: string
  65 |               description: 'An array of folder names to include. For example, ["Public", "Work"]'
  66 |             excludeFolders:
  67 |               type: array
  68 |               items:
  69 |                 type: string
  70 |               description: 'An array of folder names to exclude. For example, ["Private", "Archive"]'
  71 |             limit:
  72 |               type: number
  73 |               minimum: 1
  74 |               description: The maximum number of results to return
  75 |     SearchResponse:
  76 |       type: object
  77 |       required:
  78 |         - results
  79 |       properties:
  80 |         results:
  81 |           type: array
  82 |           items:
  83 |             type: object
  84 |             required:
  85 |               - path
  86 |               - text
  87 |               - score
  88 |               - breadcrumbs
  89 |             properties:
  90 |               path:
  91 |                 type: string
  92 |               text:
  93 |                 type: string
  94 |               score:
  95 |                 type: number
  96 |               breadcrumbs:
  97 |                 type: string
  98 |   securitySchemes:
  99 |     apiKeyAuth:
 100 |       description: |
 101 |         Find your API Key in your Obsidian settings
 102 |         in the "Local REST API" section under "Plugins".
 103 |       scheme: "bearer"
 104 |       type: "http"
 105 | info:
 106 |   description: |
 107 |     You can use this interface for trying out your Local REST API in Obsidian.
 108 | 
 109 |     Before trying the below tools, you will want to make sure you press the "Authorize" button below and provide the API Key you are shown when you open the "Local REST API" section of your Obsidian settings.  All requests to the API require a valid API Key; so you won't get very far without doing that.
 110 | 
 111 |     When using this tool you may see browser security warnings due to your browser not trusting the self-signed certificate the plugin will generate on its first run.  If you do, you can make those errors disappear by adding the certificate as a "Trusted Certificate" in your browser or operating system's settings.
 112 |   title: "Local REST API for Obsidian"
 113 |   version: "1.0"
 114 | openapi: "3.0.2"
 115 | paths:
 116 |   /:
 117 |     get:
 118 |       description: |
 119 |         Returns basic details about the server as well as your authentication status.
 120 | 
 121 |         This is the only API request that does *not* require authentication.
 122 |       responses:
 123 |         "200":
 124 |           content:
 125 |             application/json:
 126 |               schema:
 127 |                 properties:
 128 |                   authenticated:
 129 |                     description: "Is your current request authenticated?"
 130 |                     type: "boolean"
 131 |                   ok:
 132 |                     description: "'OK'"
 133 |                     type: "string"
 134 |                   service:
 135 |                     description: "'Obsidian Local REST API'"
 136 |                     type: "string"
 137 |                   versions:
 138 |                     properties:
 139 |                       obsidian:
 140 |                         description: "Obsidian plugin API version"
 141 |                         type: "string"
 142 |                       self:
 143 |                         description: "Plugin version."
 144 |                         type: "string"
 145 |                     type: "object"
 146 |                 type: "object"
 147 |           description: "Success"
 148 |       summary: |
 149 |         Returns basic details about the server.
 150 |       tags:
 151 |         - "Status"
 152 |   /active/:
 153 |     delete:
 154 |       parameters: []
 155 |       responses:
 156 |         "204":
 157 |           description: "Success"
 158 |         "404":
 159 |           content:
 160 |             application/json:
 161 |               schema:
 162 |                 "$ref": "#/components/schemas/Error"
 163 |           description: "File does not exist."
 164 |         "405":
 165 |           content:
 166 |             application/json:
 167 |               schema:
 168 |                 "$ref": "#/components/schemas/Error"
 169 |           description: |
 170 |             Your path references a directory instead of a file; this request method is valid only for updating files.
 171 |       summary: |
 172 |         Deletes the currently-active file in Obsidian.
 173 |       tags:
 174 |         - "Active File"
 175 |     get:
 176 |       description: |
 177 |         Returns the content of the currently active file in Obsidian.
 178 | 
 179 |         If you specify the header `Accept: application/vnd.olrapi.note+json`, will return a JSON representation of your note including parsed tag and frontmatter data as well as filesystem metadata.  See "responses" below for details.
 180 |       parameters: []
 181 |       responses:
 182 |         "200":
 183 |           content:
 184 |             "application/vnd.olrapi.note+json":
 185 |               schema:
 186 |                 "$ref": "#/components/schemas/NoteJson"
 187 |             text/markdown:
 188 |               schema:
 189 |                 example: |
 190 |                   # This is my document
 191 | 
 192 |                   something else here
 193 |                 type: "string"
 194 |           description: "Success"
 195 |         "404":
 196 |           description: "File does not exist"
 197 |       summary: |
 198 |         Return the content of the active file open in Obsidian.
 199 |       tags:
 200 |         - "Active File"
 201 |     patch:
 202 |       description: |
 203 |         Inserts content into the currently-open note relative to a heading within that note.
 204 | 
 205 |         Allows you to modify the content relative to a heading, block reference, or frontmatter field in your document.
 206 | 
 207 |         Note that this API was changed in Version 3.0 of this extension and the earlier PATCH API is now deprecated. Requests made using the previous version of this API will continue to work until Version 4.0 is released.  See https://github.com/coddingtonbear/obsidian-local-rest-api/wiki/Changes-to-PATCH-requests-between-versions-2.0-and-3.0 for more details and migration instructions.
 208 | 
 209 |         # Examples
 210 | 
 211 |         All of the below examples assume you have a document that looks like
 212 |         this:
 213 | 
 214 |         ```markdown
 215 |         ---
 216 |         alpha: 1
 217 |         beta: test
 218 |         delta:
 219 |         zeta: 1
 220 |         yotta: 1
 221 |         gamma:
 222 |         - one
 223 |         - two
 224 |         ---
 225 | 
 226 |         # Heading 1
 227 | 
 228 |         This is the content for heading one
 229 | 
 230 |         Also references some [[#^484ef2]]
 231 | 
 232 |         ## Subheading 1:1
 233 |         Content for Subheading 1:1
 234 | 
 235 |         ### Subsubheading 1:1:1
 236 | 
 237 |         ### Subsubheading 1:1:2
 238 | 
 239 |         Testing how block references work for a table.[[#^2c7cfa]]
 240 |         Some content for Subsubheading 1:1:2
 241 | 
 242 |         More random text.
 243 | 
 244 |         ^2d9b4a
 245 | 
 246 |         ## Subheading 1:2
 247 | 
 248 |         Content for Subheading 1:2.
 249 | 
 250 |         some content with a block reference ^484ef2
 251 | 
 252 |         ## Subheading 1:3
 253 |         | City         | Population |
 254 |         | ------------ | ---------- |
 255 |         | Seattle, WA  | 8          |
 256 |         | Portland, OR | 4          |
 257 | 
 258 |         ^2c7cfa
 259 |         ```
 260 | 
 261 |         ## Append Content Below a Heading
 262 | 
 263 |         If you wanted to append the content "Hello" below "Subheading 1:1:1" under "Heading 1",
 264 |         you could send a request with the following headers:
 265 | 
 266 |         - `Operation`: `append`
 267 |         - `Target-Type`: `heading`
 268 |         - `Target`: `Heading 1::Subheading 1:1:1`
 269 |         - with the request body: `Hello`
 270 | 
 271 |         The above would work just fine for `prepend` or `replace`, too, of course,
 272 |         but with different results.
 273 | 
 274 |         ## Append Content to a Block Reference
 275 | 
 276 |         If you wanted to append the content "Hello" below the block referenced by
 277 |         "2d9b4a" above ("More random text."), you could send the following headers:
 278 | 
 279 |         - `Operation`: `append`
 280 |         - `Target-Type`: `block`
 281 |         - `Target`: `2d9b4a`
 282 |         - with the request body: `Hello`
 283 | 
 284 |         The above would work just fine for `prepend` or `replace`, too, of course,
 285 |         but with different results.
 286 | 
 287 |         ## Add a Row to a Table Referenced by a Block Reference
 288 | 
 289 |         If you wanted to add a new city ("Chicago, IL") and population ("16") pair to the table above
 290 |         referenced by the block reference `2c7cfa`, you could send the following
 291 |         headers:
 292 | 
 293 |         - `Operation`: `append`
 294 |         - `TargetType`: `block`
 295 |         - `Target`: `2c7cfa`
 296 |         - `Content-Type`: `application/json`
 297 |         - with the request body: `[["Chicago, IL", "16"]]`
 298 | 
 299 |         The use of a `Content-Type` of `application/json` allows the API
 300 |         to infer that member of your array represents rows and columns of your
 301 |         to append to the referenced table.  You can of course just use a
 302 |         `Content-Type` of `text/markdown`, but in such a case you'll have to
 303 |         format your table row manually instead of letting the library figure
 304 |         it out for you.
 305 | 
 306 |         You also have the option of using `prepend` (in which case, your new
 307 |         row would be the first -- right below the table heading) or `replace` (in which
 308 |         case all rows except the table heading would be replaced by the new row(s)
 309 |         you supplied).
 310 | 
 311 |         ## Setting a Frontmatter Field
 312 | 
 313 |         If you wanted to set the frontmatter field `alpha` to `2`, you could
 314 |         send the following headers:
 315 | 
 316 |         - `Operation`: `replace`
 317 |         - `TargetType`: `frontmatter`
 318 |         - `Target`: `alpha`
 319 |         - with the request body `2`
 320 | 
 321 |         If you're setting a frontmatter field that might not already exist
 322 |         you may want to use the `Create-Target-If-Missing` header so the
 323 |         new frontmatter field is created and set to your specified value
 324 |         if it doesn't already exist.
 325 | 
 326 |         You may find using a `Content-Type` of `application/json` to be
 327 |         particularly useful in the case of frontmatter since frontmatter
 328 |         fields' values are JSON data, and the API can be smarter about
 329 |         interpreting your `prepend` or `append` requests if you specify
 330 |         your data as JSON (particularly when appending, for example,
 331 |         list items).
 332 |       parameters:
 333 |         - description: "Patch operation to perform"
 334 |           in: "header"
 335 |           name: "Operation"
 336 |           required: true
 337 |           schema:
 338 |             enum:
 339 |               - "append"
 340 |               - "prepend"
 341 |               - "replace"
 342 |             type: "string"
 343 |         - description: "Type of target to patch"
 344 |           in: "header"
 345 |           name: "Target-Type"
 346 |           required: true
 347 |           schema:
 348 |             enum:
 349 |               - "heading"
 350 |               - "block"
 351 |               - "frontmatter"
 352 |             type: "string"
 353 |         - description: "Delimiter to use for nested targets (i.e. Headings)"
 354 |           in: "header"
 355 |           name: "Target-Delimiter"
 356 |           required: false
 357 |           schema:
 358 |             default: "::"
 359 |             type: "string"
 360 |         - description: |
 361 |             Target to patch; this value can be URL-Encoded and *must*
 362 |             be URL-Encoded if it includes non-ASCII characters.
 363 |           in: "header"
 364 |           name: "Target"
 365 |           required: true
 366 |           schema:
 367 |             type: "string"
 368 |         - description: "Trim whitespace from Target before applying patch?"
 369 |           in: "header"
 370 |           name: "Trim-Target-Whitespace"
 371 |           required: false
 372 |           schema:
 373 |             default: "false"
 374 |             enum:
 375 |               - "true"
 376 |               - "false"
 377 |             type: "string"
 378 |       requestBody:
 379 |         content:
 380 |           application/json:
 381 |             schema:
 382 |               example: "['one', 'two']"
 383 |               type: "string"
 384 |           text/markdown:
 385 |             schema:
 386 |               example: |
 387 |                 # This is my document
 388 | 
 389 |                 something else here
 390 |               type: "string"
 391 |         description: "Content you would like to insert."
 392 |         required: true
 393 |       responses:
 394 |         "200":
 395 |           description: "Success"
 396 |         "400":
 397 |           content:
 398 |             application/json:
 399 |               schema:
 400 |                 "$ref": "#/components/schemas/Error"
 401 |           description: "Bad Request; see response message for details."
 402 |         "404":
 403 |           content:
 404 |             application/json:
 405 |               schema:
 406 |                 "$ref": "#/components/schemas/Error"
 407 |           description: "Does not exist"
 408 |         "405":
 409 |           content:
 410 |             application/json:
 411 |               schema:
 412 |                 "$ref": "#/components/schemas/Error"
 413 |           description: |
 414 |             Your path references a directory instead of a file; this request method is valid only for updating files.
 415 |       summary: |
 416 |         Insert content into the currently open note in Obsidian relative to a heading within that document.
 417 |       tags:
 418 |         - "Active File"
 419 |     post:
 420 |       description: |
 421 |         Appends content to the end of the currently-open note.
 422 | 
 423 |         If you would like to insert text relative to a particular heading instead of appending to the end of the file, see 'patch'.
 424 |       parameters: []
 425 |       requestBody:
 426 |         content:
 427 |           text/markdown:
 428 |             schema:
 429 |               example: |
 430 |                 # This is my document
 431 | 
 432 |                 something else here
 433 |               type: "string"
 434 |         description: "Content you would like to append."
 435 |         required: true
 436 |       responses:
 437 |         "204":
 438 |           description: "Success"
 439 |         "400":
 440 |           content:
 441 |             application/json:
 442 |               schema:
 443 |                 "$ref": "#/components/schemas/Error"
 444 |           description: "Bad Request"
 445 |         "405":
 446 |           content:
 447 |             application/json:
 448 |               schema:
 449 |                 "$ref": "#/components/schemas/Error"
 450 |           description: |
 451 |             Your path references a directory instead of a file; this request method is valid only for updating files.
 452 |       summary: |
 453 |         Append content to the active file open in Obsidian.
 454 |       tags:
 455 |         - "Active File"
 456 |     put:
 457 |       requestBody:
 458 |         content:
 459 |           "*/*":
 460 |             schema:
 461 |               type: "string"
 462 |           text/markdown:
 463 |             schema:
 464 |               example: |
 465 |                 # This is my document
 466 | 
 467 |                 something else here
 468 |               type: "string"
 469 |         description: "Content of the file you would like to upload."
 470 |         required: true
 471 |       responses:
 472 |         "204":
 473 |           description: "Success"
 474 |         "400":
 475 |           content:
 476 |             application/json:
 477 |               schema:
 478 |                 "$ref": "#/components/schemas/Error"
 479 |           description: |
 480 |             Incoming file could not be processed.  Make sure you have specified a reasonable file name, and make sure you have set a reasonable 'Content-Type' header; if you are uploading a note, 'text/markdown' is likely the right choice.
 481 |         "405":
 482 |           content:
 483 |             application/json:
 484 |               schema:
 485 |                 "$ref": "#/components/schemas/Error"
 486 |           description: |
 487 |             Your path references a directory instead of a file; this request method is valid only for updating files.
 488 |       summary: |
 489 |         Update the content of the active file open in Obsidian.
 490 |       tags:
 491 |         - "Active File"
 492 |   /commands/:
 493 |     get:
 494 |       responses:
 495 |         "200":
 496 |           content:
 497 |             application/json:
 498 |               example:
 499 |                 commands:
 500 |                   - id: "global-search:open"
 501 |                     name: "Search: Search in all files"
 502 |                   - id: "graph:open"
 503 |                     name: "Graph view: Open graph view"
 504 |               schema:
 505 |                 properties:
 506 |                   commands:
 507 |                     items:
 508 |                       properties:
 509 |                         id:
 510 |                           type: "string"
 511 |                         name:
 512 |                           type: "string"
 513 |                       type: "object"
 514 |                     type: "array"
 515 |                 type: "object"
 516 |           description: "A list of available commands."
 517 |       summary: |
 518 |         Get a list of available commands.
 519 |       tags:
 520 |         - "Commands"
 521 |   "/commands/{commandId}/":
 522 |     post:
 523 |       parameters:
 524 |         - description: "The id of the command to execute"
 525 |           in: "path"
 526 |           name: "commandId"
 527 |           required: true
 528 |           schema:
 529 |             type: "string"
 530 |       responses:
 531 |         "204":
 532 |           description: "Success"
 533 |         "404":
 534 |           content:
 535 |             application/json:
 536 |               schema:
 537 |                 "$ref": "#/components/schemas/Error"
 538 |           description: "The command you specified does not exist."
 539 |       summary: |
 540 |         Execute a command.
 541 |       tags:
 542 |         - "Commands"
 543 |   "/open/{filename}":
 544 |     post:
 545 |       description: |
 546 |         Opens the specified document in Obsidian.
 547 | 
 548 |         Note: Obsidian will create a new document at the path you have
 549 |         specified if such a document did not already exist.
 550 |       parameters:
 551 |         - description: |
 552 |             Path to the file to return (relative to your vault root).
 553 |           in: "path"
 554 |           name: "filename"
 555 |           required: true
 556 |           schema:
 557 |             format: "path"
 558 |             type: "string"
 559 |         - description: "Open this as a new leaf?"
 560 |           in: "query"
 561 |           name: "newLeaf"
 562 |           required: false
 563 |           schema:
 564 |             type: "boolean"
 565 |       responses:
 566 |         "200":
 567 |           description: "Success"
 568 |       summary: |
 569 |         Open the specified document in Obsidian
 570 |       tags:
 571 |         - "Open"
 572 |   "/periodic/{period}/":
 573 |     delete:
 574 |       description: |
 575 |         Deletes the periodic note for the specified period.
 576 |       parameters:
 577 |         - description: "The name of the period for which you would like to grab the current note."
 578 |           in: "path"
 579 |           name: "period"
 580 |           required: true
 581 |           schema:
 582 |             default: "daily"
 583 |             enum:
 584 |               - "daily"
 585 |               - "weekly"
 586 |               - "monthly"
 587 |               - "quarterly"
 588 |               - "yearly"
 589 |             type: "string"
 590 |       responses:
 591 |         "204":
 592 |           description: "Success"
 593 |         "404":
 594 |           content:
 595 |             application/json:
 596 |               schema:
 597 |                 "$ref": "#/components/schemas/Error"
 598 |           description: "File does not exist."
 599 |         "405":
 600 |           content:
 601 |             application/json:
 602 |               schema:
 603 |                 "$ref": "#/components/schemas/Error"
 604 |           description: |
 605 |             Your path references a directory instead of a file; this request method is valid only for updating files.
 606 |       summary: |
 607 |         Delete a periodic note.
 608 |       tags:
 609 |         - "Periodic Notes"
 610 |     get:
 611 |       parameters:
 612 |         - description: "The name of the period for which you would like to grab the current note."
 613 |           in: "path"
 614 |           name: "period"
 615 |           required: true
 616 |           schema:
 617 |             default: "daily"
 618 |             enum:
 619 |               - "daily"
 620 |               - "weekly"
 621 |               - "monthly"
 622 |               - "quarterly"
 623 |               - "yearly"
 624 |             type: "string"
 625 |       responses:
 626 |         "200":
 627 |           content:
 628 |             "application/vnd.olrapi.note+json":
 629 |               schema:
 630 |                 "$ref": "#/components/schemas/NoteJson"
 631 |             text/markdown:
 632 |               schema:
 633 |                 example: |
 634 |                   # This is my document
 635 | 
 636 |                   something else here
 637 |                 type: "string"
 638 |           description: "Success"
 639 |         "404":
 640 |           description: "File does not exist"
 641 |       summary: |
 642 |         Get current periodic note for the specified period.
 643 |       tags:
 644 |         - "Periodic Notes"
 645 |     patch:
 646 |       description: |
 647 |         Inserts content into an existing note relative to a heading within your note.
 648 | 
 649 |         Allows you to modify the content relative to a heading, block reference, or frontmatter field in your document.
 650 | 
 651 |         Note that this API was changed in Version 3.0 of this extension and the earlier PATCH API is now deprecated. Requests made using the previous version of this API will continue to work until Version 4.0 is released.  See https://github.com/coddingtonbear/obsidian-local-rest-api/wiki/Changes-to-PATCH-requests-between-versions-2.0-and-3.0 for more details and migration instructions.
 652 | 
 653 |         # Examples
 654 | 
 655 |         All of the below examples assume you have a document that looks like
 656 |         this:
 657 | 
 658 |         ```markdown
 659 |         ---
 660 |         alpha: 1
 661 |         beta: test
 662 |         delta:
 663 |         zeta: 1
 664 |         yotta: 1
 665 |         gamma:
 666 |         - one
 667 |         - two
 668 |         ---
 669 | 
 670 |         # Heading 1
 671 | 
 672 |         This is the content for heading one
 673 | 
 674 |         Also references some [[#^484ef2]]
 675 | 
 676 |         ## Subheading 1:1
 677 |         Content for Subheading 1:1
 678 | 
 679 |         ### Subsubheading 1:1:1
 680 | 
 681 |         ### Subsubheading 1:1:2
 682 | 
 683 |         Testing how block references work for a table.[[#^2c7cfa]]
 684 |         Some content for Subsubheading 1:1:2
 685 | 
 686 |         More random text.
 687 | 
 688 |         ^2d9b4a
 689 | 
 690 |         ## Subheading 1:2
 691 | 
 692 |         Content for Subheading 1:2.
 693 | 
 694 |         some content with a block reference ^484ef2
 695 | 
 696 |         ## Subheading 1:3
 697 |         | City         | Population |
 698 |         | ------------ | ---------- |
 699 |         | Seattle, WA  | 8          |
 700 |         | Portland, OR | 4          |
 701 | 
 702 |         ^2c7cfa
 703 |         ```
 704 | 
 705 |         ## Append Content Below a Heading
 706 | 
 707 |         If you wanted to append the content "Hello" below "Subheading 1:1:1" under "Heading 1",
 708 |         you could send a request with the following headers:
 709 | 
 710 |         - `Operation`: `append`
 711 |         - `Target-Type`: `heading`
 712 |         - `Target`: `Heading 1::Subheading 1:1:1`
 713 |         - with the request body: `Hello`
 714 | 
 715 |         The above would work just fine for `prepend` or `replace`, too, of course,
 716 |         but with different results.
 717 | 
 718 |         ## Append Content to a Block Reference
 719 | 
 720 |         If you wanted to append the content "Hello" below the block referenced by
 721 |         "2d9b4a" above ("More random text."), you could send the following headers:
 722 | 
 723 |         - `Operation`: `append`
 724 |         - `Target-Type`: `block`
 725 |         - `Target`: `2d9b4a`
 726 |         - with the request body: `Hello`
 727 | 
 728 |         The above would work just fine for `prepend` or `replace`, too, of course,
 729 |         but with different results.
 730 | 
 731 |         ## Add a Row to a Table Referenced by a Block Reference
 732 | 
 733 |         If you wanted to add a new city ("Chicago, IL") and population ("16") pair to the table above
 734 |         referenced by the block reference `2c7cfa`, you could send the following
 735 |         headers:
 736 | 
 737 |         - `Operation`: `append`
 738 |         - `TargetType`: `block`
 739 |         - `Target`: `2c7cfa`
 740 |         - `Content-Type`: `application/json`
 741 |         - with the request body: `[["Chicago, IL", "16"]]`
 742 | 
 743 |         The use of a `Content-Type` of `application/json` allows the API
 744 |         to infer that member of your array represents rows and columns of your
 745 |         to append to the referenced table.  You can of course just use a
 746 |         `Content-Type` of `text/markdown`, but in such a case you'll have to
 747 |         format your table row manually instead of letting the library figure
 748 |         it out for you.
 749 | 
 750 |         You also have the option of using `prepend` (in which case, your new
 751 |         row would be the first -- right below the table heading) or `replace` (in which
 752 |         case all rows except the table heading would be replaced by the new row(s)
 753 |         you supplied).
 754 | 
 755 |         ## Setting a Frontmatter Field
 756 | 
 757 |         If you wanted to set the frontmatter field `alpha` to `2`, you could
 758 |         send the following headers:
 759 | 
 760 |         - `Operation`: `replace`
 761 |         - `TargetType`: `frontmatter`
 762 |         - `Target`: `beep`
 763 |         - with the request body `2`
 764 | 
 765 |         If you're setting a frontmatter field that might not already exist
 766 |         you may want to use the `Create-Target-If-Missing` header so the
 767 |         new frontmatter field is created and set to your specified value
 768 |         if it doesn't already exist.
 769 | 
 770 |         You may find using a `Content-Type` of `application/json` to be
 771 |         particularly useful in the case of frontmatter since frontmatter
 772 |         fields' values are JSON data, and the API can be smarter about
 773 |         interpreting yoru `prepend` or `append` requests if you specify
 774 |         your data as JSON (particularly when appending, for example,
 775 |         list items).
 776 |       parameters:
 777 |         - description: "Patch operation to perform"
 778 |           in: "header"
 779 |           name: "Operation"
 780 |           required: true
 781 |           schema:
 782 |             enum:
 783 |               - "append"
 784 |               - "prepend"
 785 |               - "replace"
 786 |             type: "string"
 787 |         - description: "Type of target to patch"
 788 |           in: "header"
 789 |           name: "Target-Type"
 790 |           required: true
 791 |           schema:
 792 |             enum:
 793 |               - "heading"
 794 |               - "block"
 795 |               - "frontmatter"
 796 |             type: "string"
 797 |         - description: "Delimiter to use for nested targets (i.e. Headings)"
 798 |           in: "header"
 799 |           name: "Target-Delimiter"
 800 |           required: false
 801 |           schema:
 802 |             default: "::"
 803 |             type: "string"
 804 |         - description: |
 805 |             Target to patch; this value can be URL-Encoded and *must*
 806 |             be URL-Encoded if it includes non-ASCII characters.
 807 |           in: "header"
 808 |           name: "Target"
 809 |           required: true
 810 |           schema:
 811 |             type: "string"
 812 |         - description: "Trim whitespace from Target before applying patch?"
 813 |           in: "header"
 814 |           name: "Trim-Target-Whitespace"
 815 |           required: false
 816 |           schema:
 817 |             default: "false"
 818 |             enum:
 819 |               - "true"
 820 |               - "false"
 821 |             type: "string"
 822 |         - description: "The name of the period for which you would like to grab the current note."
 823 |           in: "path"
 824 |           name: "period"
 825 |           required: true
 826 |           schema:
 827 |             default: "daily"
 828 |             enum:
 829 |               - "daily"
 830 |               - "weekly"
 831 |               - "monthly"
 832 |               - "quarterly"
 833 |               - "yearly"
 834 |             type: "string"
 835 |       requestBody:
 836 |         content:
 837 |           application/json:
 838 |             schema:
 839 |               example: "['one', 'two']"
 840 |               type: "string"
 841 |           text/markdown:
 842 |             schema:
 843 |               example: |
 844 |                 # This is my document
 845 | 
 846 |                 something else here
 847 |               type: "string"
 848 |         description: "Content you would like to insert."
 849 |         required: true
 850 |       responses:
 851 |         "200":
 852 |           description: "Success"
 853 |         "400":
 854 |           content:
 855 |             application/json:
 856 |               schema:
 857 |                 "$ref": "#/components/schemas/Error"
 858 |           description: "Bad Request; see response message for details."
 859 |         "404":
 860 |           content:
 861 |             application/json:
 862 |               schema:
 863 |                 "$ref": "#/components/schemas/Error"
 864 |           description: "Does not exist"
 865 |         "405":
 866 |           content:
 867 |             application/json:
 868 |               schema:
 869 |                 "$ref": "#/components/schemas/Error"
 870 |           description: |
 871 |             Your path references a directory instead of a file; this request method is valid only for updating files.
 872 |       summary: |
 873 |         Insert content into a periodic note relative to a heading within that document.
 874 |       tags:
 875 |         - "Periodic Notes"
 876 |     post:
 877 |       description: |
 878 |         Appends content to the periodic note for the specified period.  This will create the relevant periodic note if necessary.
 879 |       parameters:
 880 |         - description: "The name of the period for which you would like to grab the current note."
 881 |           in: "path"
 882 |           name: "period"
 883 |           required: true
 884 |           schema:
 885 |             default: "daily"
 886 |             enum:
 887 |               - "daily"
 888 |               - "weekly"
 889 |               - "monthly"
 890 |               - "quarterly"
 891 |               - "yearly"
 892 |             type: "string"
 893 |       requestBody:
 894 |         content:
 895 |           text/markdown:
 896 |             schema:
 897 |               example: |
 898 |                 # This is my document
 899 | 
 900 |                 something else here
 901 |               type: "string"
 902 |         description: "Content you would like to append."
 903 |         required: true
 904 |       responses:
 905 |         "204":
 906 |           description: "Success"
 907 |         "400":
 908 |           content:
 909 |             application/json:
 910 |               schema:
 911 |                 "$ref": "#/components/schemas/Error"
 912 |           description: "Bad Request"
 913 |         "405":
 914 |           content:
 915 |             application/json:
 916 |               schema:
 917 |                 "$ref": "#/components/schemas/Error"
 918 |           description: |
 919 |             Your path references a directory instead of a file; this request method is valid only for updating files.
 920 |       summary: |
 921 |         Append content to a periodic note.
 922 |       tags:
 923 |         - "Periodic Notes"
 924 |     put:
 925 |       parameters:
 926 |         - description: "The name of the period for which you would like to grab the current note."
 927 |           in: "path"
 928 |           name: "period"
 929 |           required: true
 930 |           schema:
 931 |             default: "daily"
 932 |             enum:
 933 |               - "daily"
 934 |               - "weekly"
 935 |               - "monthly"
 936 |               - "quarterly"
 937 |               - "yearly"
 938 |             type: "string"
 939 |       requestBody:
 940 |         content:
 941 |           "*/*":
 942 |             schema:
 943 |               type: "string"
 944 |           text/markdown:
 945 |             schema:
 946 |               example: |
 947 |                 # This is my document
 948 | 
 949 |                 something else here
 950 |               type: "string"
 951 |         description: "Content of the file you would like to upload."
 952 |         required: true
 953 |       responses:
 954 |         "204":
 955 |           description: "Success"
 956 |         "400":
 957 |           content:
 958 |             application/json:
 959 |               schema:
 960 |                 "$ref": "#/components/schemas/Error"
 961 |           description: |
 962 |             Incoming file could not be processed.  Make sure you have specified a reasonable file name, and make sure you have set a reasonable 'Content-Type' header; if you are uploading a note, 'text/markdown' is likely the right choice.
 963 |         "405":
 964 |           content:
 965 |             application/json:
 966 |               schema:
 967 |                 "$ref": "#/components/schemas/Error"
 968 |           description: |
 969 |             Your path references a directory instead of a file; this request method is valid only for updating files.
 970 |       summary: |
 971 |         Update the content of a periodic note.
 972 |       tags:
 973 |         - "Periodic Notes"
 974 |   /search/:
 975 |     post:
 976 |       description: |
 977 |         Evaluates a provided query against each file in your vault.
 978 | 
 979 |         This endpoint supports multiple query formats.  Your query should be specified in your request's body, and will be interpreted according to the `Content-type` header you specify from the below options.Additional query formats may be added in the future.
 980 | 
 981 |         # Dataview DQL (`application/vnd.olrapi.dataview.dql+txt`)
 982 | 
 983 |         Accepts a `TABLE`-type Dataview query as a text string.  See [Dataview](https://blacksmithgu.github.io/obsidian-dataview/query/queries/)'s query documentation for information on how to construct a query.
 984 | 
 985 |         # JsonLogic (`application/vnd.olrapi.jsonlogic+json`)
 986 | 
 987 |         Accepts a JsonLogic query specified as JSON.  See [JsonLogic](https://jsonlogic.com/operations.html)'s documentation for information about the base set of operators available, but in addition to those operators the following operators are available:
 988 | 
 989 |         - `glob: [PATTERN, VALUE]`: Returns `true` if a string matches a glob pattern.  E.g.: `{"glob": ["*.foo", "bar.foo"]}` is `true` and `{"glob": ["*.bar", "bar.foo"]}` is `false`.
 990 |         - `regexp: [PATTERN, VALUE]`: Returns `true` if a string matches a regular expression.  E.g.: `{"regexp": [".*\.foo", "bar.foo"]` is `true` and `{"regexp": [".*\.bar", "bar.foo"]}` is `false`.
 991 | 
 992 |         Returns only non-falsy results.  "Non-falsy" here treats the following values as "falsy":
 993 | 
 994 |         - `false`
 995 |         - `null` or `undefined`
 996 |         - `0`
 997 |         - `[]`
 998 |         - `{}`
 999 | 
1000 |         Files are represented as an object having the schema described
1001 |         in the Schema named 'NoteJson' at the bottom of this page.
1002 |         Understanding the shape of a JSON object from a schema can be
1003 |         tricky; so you may find it helpful to examine the generated metadata
1004 |         for individual files in your vault to understand exactly what values
1005 |         are returned.  To see that, access the `GET` `/vault/{filePath}`
1006 |         route setting the header:
1007 |         `Accept: application/vnd.olrapi.note+json`.  See examples below
1008 |         for working examples of queries performing common search operations.
1009 |       requestBody:
1010 |         content:
1011 |           "application/vnd.olrapi.dataview.dql+txt":
1012 |             examples:
1013 |               find_fields_by_tag:
1014 |                 summary: "List data from files having the #game tag."
1015 |                 value: |
1016 |                   TABLE
1017 |                     time-played AS "Time Played",
1018 |                     length AS "Length",
1019 |                     rating AS "Rating"
1020 |                   FROM #game
1021 |                   SORT rating DESC
1022 |             schema:
1023 |               externalDocs:
1024 |                 url: "https://blacksmithgu.github.io/obsidian-dataview/query/queries/"
1025 |               type: "object"
1026 |           "application/vnd.olrapi.jsonlogic+json":
1027 |             examples:
1028 |               find_by_frontmatter_url_glob:
1029 |                 summary: "Find notes having URL or a matching URL glob frontmatter field."
1030 |                 value: |
1031 |                   {
1032 |                     "or": [
1033 |                       {"===": [{"var": "frontmatter.url"}, "https://myurl.com/some/path/"]},
1034 |                       {"glob": [{"var": "frontmatter.url-glob"}, "https://myurl.com/some/path/"]}
1035 |                     ]
1036 |                   }
1037 |               find_by_frontmatter_value:
1038 |                 summary: "Find notes having a certain frontmatter field value."
1039 |                 value: |
1040 |                   {
1041 |                     "==": [
1042 |                       {"var": "frontmatter.myField"},
1043 |                       "myValue"
1044 |                     ]
1045 |                   }
1046 |               find_by_tag:
1047 |                 summary: "Find notes having a certain tag"
1048 |                 value: |
1049 |                   {
1050 |                     "in": [
1051 |                       "myTag",
1052 |                       {"var": "tags"}
1053 |                     ]
1054 |                   }
1055 |             schema:
1056 |               externalDocs:
1057 |                 url: "https://jsonlogic.com/operations.html"
1058 |               type: "object"
1059 |         required: true
1060 |       responses:
1061 |         "200":
1062 |           content:
1063 |             application/json:
1064 |               schema:
1065 |                 items:
1066 |                   properties:
1067 |                     filename:
1068 |                       description: "Path to the matching file"
1069 |                       type: "string"
1070 |                     result:
1071 |                       oneOf:
1072 |                         - type: "string"
1073 |                         - type: "number"
1074 |                         - type: "array"
1075 |                         - type: "object"
1076 |                         - type: "boolean"
1077 |                   required:
1078 |                     - "filename"
1079 |                     - "result"
1080 |                   type: "object"
1081 |                 type: "array"
1082 |           description: "Success"
1083 |         "400":
1084 |           content:
1085 |             application/json:
1086 |               schema:
1087 |                 "$ref": "#/components/schemas/Error"
1088 |           description: |
1089 |             Bad request.  Make sure you have specified an acceptable
1090 |             Content-Type for your search query.
1091 |       summary: |
1092 |         Search for documents matching a specified search query
1093 |       tags:
1094 |         - "Search"
1095 |   /search/simple/:
1096 |     post:
1097 |       parameters:
1098 |         - description: "Your search query"
1099 |           in: "query"
1100 |           name: "query"
1101 |           required: true
1102 |           schema:
1103 |             type: "string"
1104 |         - description: "How much context to return around the matching string"
1105 |           in: "query"
1106 |           name: "contextLength"
1107 |           required: false
1108 |           schema:
1109 |             default: 100
1110 |             type: "number"
1111 |       responses:
1112 |         "200":
1113 |           content:
1114 |             application/json:
1115 |               schema:
1116 |                 items:
1117 |                   properties:
1118 |                     filename:
1119 |                       description: "Path to the matching file"
1120 |                       type: "string"
1121 |                     matches:
1122 |                       items:
1123 |                         properties:
1124 |                           context:
1125 |                             type: "string"
1126 |                           match:
1127 |                             properties:
1128 |                               end:
1129 |                                 type: "number"
1130 |                               start:
1131 |                                 type: "number"
1132 |                             required:
1133 |                               - "start"
1134 |                               - "end"
1135 |                             type: "object"
1136 |                         required:
1137 |                           - "match"
1138 |                           - "context"
1139 |                         type: "object"
1140 |                       type: "array"
1141 |                     score:
1142 |                       type: "number"
1143 |                   type: "object"
1144 |                 type: "array"
1145 |           description: "Success"
1146 |       summary: |
1147 |         Search for documents matching a specified text query
1148 |       tags:
1149 |         - "Search"
1150 |   /search/smart:
1151 |     post:
1152 |       summary: "Perform a smart search"
1153 |       description: "Handles semantic search requests."
1154 |       requestBody:
1155 |         required: true
1156 |         content:
1157 |           application/json:
1158 |             schema:
1159 |               $ref: "#/components/schemas/SearchParameters"
1160 |       responses:
1161 |         "200":
1162 |           description: "Successful search"
1163 |           content:
1164 |             application/json:
1165 |               schema:
1166 |                 $ref: "#/components/schemas/SearchResponse"
1167 |         "400":
1168 |           description: "Invalid request body"
1169 |           content:
1170 |             application/json:
1171 |               schema:
1172 |                 $ref: "#/components/schemas/Error"
1173 |         "503":
1174 |           description: "Service unavailable"
1175 |           content:
1176 |             application/json:
1177 |               schema:
1178 |                 $ref: "#/components/schemas/Error"
1179 |   /vault/:
1180 |     get:
1181 |       description: |
1182 |         Lists files in the root directory of your vault.
1183 | 
1184 |         Note: that this is exactly the same API endpoint as the below "List files that exist in the specified directory." and exists here only due to a quirk of this particular interactive tool.
1185 |       responses:
1186 |         "200":
1187 |           content:
1188 |             application/json:
1189 |               example:
1190 |                 files:
1191 |                   - "mydocument.md"
1192 |                   - "somedirectory/"
1193 |               schema:
1194 |                 properties:
1195 |                   files:
1196 |                     items:
1197 |                       type: "string"
1198 |                     type: "array"
1199 |                 type: "object"
1200 |           description: "Success"
1201 |         "404":
1202 |           content:
1203 |             application/json:
1204 |               schema:
1205 |                 "$ref": "#/components/schemas/Error"
1206 |           description: "Directory does not exist"
1207 |       summary: |
1208 |         List files that exist in the root of your vault.
1209 |       tags:
1210 |         - "Vault Directories"
1211 |   "/vault/{filename}":
1212 |     delete:
1213 |       parameters:
1214 |         - description: |
1215 |             Path to the relevant file (relative to your vault root).
1216 |           in: "path"
1217 |           name: "filename"
1218 |           required: true
1219 |           schema:
1220 |             format: "path"
1221 |             type: "string"
1222 |       responses:
1223 |         "204":
1224 |           description: "Success"
1225 |         "404":
1226 |           content:
1227 |             application/json:
1228 |               schema:
1229 |                 "$ref": "#/components/schemas/Error"
1230 |           description: "File does not exist."
1231 |         "405":
1232 |           content:
1233 |             application/json:
1234 |               schema:
1235 |                 "$ref": "#/components/schemas/Error"
1236 |           description: |
1237 |             Your path references a directory instead of a file; this request method is valid only for updating files.
1238 |       summary: |
1239 |         Delete a particular file in your vault.
1240 |       tags:
1241 |         - "Vault Files"
1242 |     get:
1243 |       description: |
1244 |         Returns the content of the file at the specified path in your vault should the file exist.
1245 | 
1246 |         If you specify the header `Accept: application/vnd.olrapi.note+json`, will return a JSON representation of your note including parsed tag and frontmatter data as well as filesystem metadata.  See "responses" below for details.
1247 |       parameters:
1248 |         - description: |
1249 |             Path to the relevant file (relative to your vault root).
1250 |           in: "path"
1251 |           name: "filename"
1252 |           required: true
1253 |           schema:
1254 |             format: "path"
1255 |             type: "string"
1256 |       responses:
1257 |         "200":
1258 |           content:
1259 |             "application/vnd.olrapi.note+json":
1260 |               schema:
1261 |                 "$ref": "#/components/schemas/NoteJson"
1262 |             text/markdown:
1263 |               schema:
1264 |                 example: |
1265 |                   # This is my document
1266 | 
1267 |                   something else here
1268 |                 type: "string"
1269 |           description: "Success"
1270 |         "404":
1271 |           description: "File does not exist"
1272 |       summary: |
1273 |         Return the content of a single file in your vault.
1274 |       tags:
1275 |         - "Vault Files"
1276 |     patch:
1277 |       description: |
1278 |         Inserts content into an existing note relative to a heading within your note.
1279 | 
1280 |         Allows you to modify the content relative to a heading, block reference, or frontmatter field in your document.
1281 | 
1282 |         Note that this API was changed in Version 3.0 of this extension and the earlier PATCH API is now deprecated. Requests made using the previous version of this API will continue to work until Version 4.0 is released.  See https://github.com/coddingtonbear/obsidian-local-rest-api/wiki/Changes-to-PATCH-requests-between-versions-2.0-and-3.0 for more details and migration instructions.
1283 | 
1284 |         # Examples
1285 | 
1286 |         All of the below examples assume you have a document that looks like
1287 |         this:
1288 | 
1289 |         ```markdown
1290 |         ---
1291 |         alpha: 1
1292 |         beta: test
1293 |         delta:
1294 |         zeta: 1
1295 |         yotta: 1
1296 |         gamma:
1297 |         - one
1298 |         - two
1299 |         ---
1300 | 
1301 |         # Heading 1
1302 | 
1303 |         This is the content for heading one
1304 | 
1305 |         Also references some [[#^484ef2]]
1306 | 
1307 |         ## Subheading 1:1
1308 |         Content for Subheading 1:1
1309 | 
1310 |         ### Subsubheading 1:1:1
1311 | 
1312 |         ### Subsubheading 1:1:2
1313 | 
1314 |         Testing how block references work for a table.[[#^2c7cfa]]
1315 |         Some content for Subsubheading 1:1:2
1316 | 
1317 |         More random text.
1318 | 
1319 |         ^2d9b4a
1320 | 
1321 |         ## Subheading 1:2
1322 | 
1323 |         Content for Subheading 1:2.
1324 | 
1325 |         some content with a block reference ^484ef2
1326 | 
1327 |         ## Subheading 1:3
1328 |         | City         | Population |
1329 |         | ------------ | ---------- |
1330 |         | Seattle, WA  | 8          |
1331 |         | Portland, OR | 4          |
1332 | 
1333 |         ^2c7cfa
1334 |         ```
1335 | 
1336 |         ## Append Content Below a Heading
1337 | 
1338 |         If you wanted to append the content "Hello" below "Subheading 1:1:1" under "Heading 1",
1339 |         you could send a request with the following headers:
1340 | 
1341 |         - `Operation`: `append`
1342 |         - `Target-Type`: `heading`
1343 |         - `Target`: `Heading 1::Subheading 1:1:1`
1344 |         - with the request body: `Hello`
1345 | 
1346 |         The above would work just fine for `prepend` or `replace`, too, of course,
1347 |         but with different results.
1348 | 
1349 |         ## Append Content to a Block Reference
1350 | 
1351 |         If you wanted to append the content "Hello" below the block referenced by
1352 |         "2d9b4a" above ("More random text."), you could send the following headers:
1353 | 
1354 |         - `Operation`: `append`
1355 |         - `Target-Type`: `block`
1356 |         - `Target`: `2d9b4a`
1357 |         - with the request body: `Hello`
1358 | 
1359 |         The above would work just fine for `prepend` or `replace`, too, of course,
1360 |         but with different results.
1361 | 
1362 |         ## Add a Row to a Table Referenced by a Block Reference
1363 | 
1364 |         If you wanted to add a new city ("Chicago, IL") and population ("16") pair to the table above
1365 |         referenced by the block reference `2c7cfa`, you could send the following
1366 |         headers:
1367 | 
1368 |         - `Operation`: `append`
1369 |         - `TargetType`: `block`
1370 |         - `Target`: `2c7cfa`
1371 |         - `Content-Type`: `application/json`
1372 |         - with the request body: `[["Chicago, IL", "16"]]`
1373 | 
1374 |         The use of a `Content-Type` of `application/json` allows the API
1375 |         to infer that member of your array represents rows and columns of your
1376 |         to append to the referenced table.  You can of course just use a
1377 |         `Content-Type` of `text/markdown`, but in such a case you'll have to
1378 |         format your table row manually instead of letting the library figure
1379 |         it out for you.
1380 | 
1381 |         You also have the option of using `prepend` (in which case, your new
1382 |         row would be the first -- right below the table heading) or `replace` (in which
1383 |         case all rows except the table heading would be replaced by the new row(s)
1384 |         you supplied).
1385 | 
1386 |         ## Setting a Frontmatter Field
1387 | 
1388 |         If you wanted to set the frontmatter field `alpha` to `2`, you could
1389 |         send the following headers:
1390 | 
1391 |         - `Operation`: `replace`
1392 |         - `TargetType`: `frontmatter`
1393 |         - `Target`: `beep`
1394 |         - with the request body `2`
1395 | 
1396 |         If you're setting a frontmatter field that might not already exist
1397 |         you may want to use the `Create-Target-If-Missing` header so the
1398 |         new frontmatter field is created and set to your specified value
1399 |         if it doesn't already exist.
1400 | 
1401 |         You may find using a `Content-Type` of `application/json` to be
1402 |         particularly useful in the case of frontmatter since frontmatter
1403 |         fields' values are JSON data, and the API can be smarter about
1404 |         interpreting yoru `prepend` or `append` requests if you specify
1405 |         your data as JSON (particularly when appending, for example,
1406 |         list items).
1407 |       parameters:
1408 |         - description: "Patch operation to perform"
1409 |           in: "header"
1410 |           name: "Operation"
1411 |           required: true
1412 |           schema:
1413 |             enum:
1414 |               - "append"
1415 |               - "prepend"
1416 |               - "replace"
1417 |             type: "string"
1418 |         - description: "Type of target to patch"
1419 |           in: "header"
1420 |           name: "Target-Type"
1421 |           required: true
1422 |           schema:
1423 |             enum:
1424 |               - "heading"
1425 |               - "block"
1426 |               - "frontmatter"
1427 |             type: "string"
1428 |         - description: "Delimiter to use for nested targets (i.e. Headings)"
1429 |           in: "header"
1430 |           name: "Target-Delimiter"
1431 |           required: false
1432 |           schema:
1433 |             default: "::"
1434 |             type: "string"
1435 |         - description: |
1436 |             Target to patch; this value can be URL-Encoded and *must*
1437 |             be URL-Encoded if it includes non-ASCII characters.
1438 |           in: "header"
1439 |           name: "Target"
1440 |           required: true
1441 |           schema:
1442 |             type: "string"
1443 |         - description: "Trim whitespace from Target before applying patch?"
1444 |           in: "header"
1445 |           name: "Trim-Target-Whitespace"
1446 |           required: false
1447 |           schema:
1448 |             default: "false"
1449 |             enum:
1450 |               - "true"
1451 |               - "false"
1452 |             type: "string"
1453 |         - description: |
1454 |             Path to the relevant file (relative to your vault root).
1455 |           in: "path"
1456 |           name: "filename"
1457 |           required: true
1458 |           schema:
1459 |             format: "path"
1460 |             type: "string"
1461 |       requestBody:
1462 |         content:
1463 |           application/json:
1464 |             schema:
1465 |               example: "['one', 'two']"
1466 |               type: "string"
1467 |           text/markdown:
1468 |             schema:
1469 |               example: |
1470 |                 # This is my document
1471 | 
1472 |                 something else here
1473 |               type: "string"
1474 |         description: "Content you would like to insert."
1475 |         required: true
1476 |       responses:
1477 |         "200":
1478 |           description: "Success"
1479 |         "400":
1480 |           content:
1481 |             application/json:
1482 |               schema:
1483 |                 "$ref": "#/components/schemas/Error"
1484 |           description: "Bad Request; see response message for details."
1485 |         "404":
1486 |           content:
1487 |             application/json:
1488 |               schema:
1489 |                 "$ref": "#/components/schemas/Error"
1490 |           description: "Does not exist"
1491 |         "405":
1492 |           content:
1493 |             application/json:
1494 |               schema:
1495 |                 "$ref": "#/components/schemas/Error"
1496 |           description: |
1497 |             Your path references a directory instead of a file; this request method is valid only for updating files.
1498 |       summary: |
1499 |         Insert content into an existing note relative to a heading within that document.
1500 |       tags:
1501 |         - "Vault Files"
1502 |     post:
1503 |       description: |
1504 |         Appends content to the end of an existing note. If the specified file does not yet exist, it will be created as an empty file.
1505 | 
1506 |         If you would like to insert text relative to a particular heading instead of appending to the end of the file, see 'patch'.
1507 |       parameters:
1508 |         - description: |
1509 |             Path to the relevant file (relative to your vault root).
1510 |           in: "path"
1511 |           name: "filename"
1512 |           required: true
1513 |           schema:
1514 |             format: "path"
1515 |             type: "string"
1516 |       requestBody:
1517 |         content:
1518 |           text/markdown:
1519 |             schema:
1520 |               example: |
1521 |                 # This is my document
1522 | 
1523 |                 something else here
1524 |               type: "string"
1525 |         description: "Content you would like to append."
1526 |         required: true
1527 |       responses:
1528 |         "204":
1529 |           description: "Success"
1530 |         "400":
1531 |           content:
1532 |             application/json:
1533 |               schema:
1534 |                 "$ref": "#/components/schemas/Error"
1535 |           description: "Bad Request"
1536 |         "405":
1537 |           content:
1538 |             application/json:
1539 |               schema:
1540 |                 "$ref": "#/components/schemas/Error"
1541 |           description: |
1542 |             Your path references a directory instead of a file; this request method is valid only for updating files.
1543 |       summary: |
1544 |         Append content to a new or existing file.
1545 |       tags:
1546 |         - "Vault Files"
1547 |     put:
1548 |       description: |
1549 |         Creates a new file in your vault or updates the content of an existing one if the specified file already exists.
1550 |       parameters:
1551 |         - description: |
1552 |             Path to the relevant file (relative to your vault root).
1553 |           in: "path"
1554 |           name: "filename"
1555 |           required: true
1556 |           schema:
1557 |             format: "path"
1558 |             type: "string"
1559 |       requestBody:
1560 |         content:
1561 |           "*/*":
1562 |             schema:
1563 |               type: "string"
1564 |           text/markdown:
1565 |             schema:
1566 |               example: |
1567 |                 # This is my document
1568 | 
1569 |                 something else here
1570 |               type: "string"
1571 |         description: "Content of the file you would like to upload."
1572 |         required: true
1573 |       responses:
1574 |         "204":
1575 |           description: "Success"
1576 |         "400":
1577 |           content:
1578 |             application/json:
1579 |               schema:
1580 |                 "$ref": "#/components/schemas/Error"
1581 |           description: |
1582 |             Incoming file could not be processed.  Make sure you have specified a reasonable file name, and make sure you have set a reasonable 'Content-Type' header; if you are uploading a note, 'text/markdown' is likely the right choice.
1583 |         "405":
1584 |           content:
1585 |             application/json:
1586 |               schema:
1587 |                 "$ref": "#/components/schemas/Error"
1588 |           description: |
1589 |             Your path references a directory instead of a file; this request method is valid only for updating files.
1590 |       summary: |
1591 |         Create a new file in your vault or update the content of an existing one.
1592 |       tags:
1593 |         - "Vault Files"
1594 |   "/vault/{pathToDirectory}/":
1595 |     get:
1596 |       parameters:
1597 |         - description: |
1598 |             Path to list files from (relative to your vault root).  Note that empty directories will not be returned.
1599 | 
1600 |             Note: this particular interactive tool requires that you provide an argument for this field, but the API itself will allow you to list the root folder of your vault. If you would like to try listing content in the root of your vault using this interactive tool, use the above "List files that exist in the root of your vault" form above.
1601 |           in: "path"
1602 |           name: "pathToDirectory"
1603 |           required: true
1604 |           schema:
1605 |             format: "path"
1606 |             type: "string"
1607 |       responses:
1608 |         "200":
1609 |           content:
1610 |             application/json:
1611 |               example:
1612 |                 files:
1613 |                   - "mydocument.md"
1614 |                   - "somedirectory/"
1615 |               schema:
1616 |                 properties:
1617 |                   files:
1618 |                     items:
1619 |                       type: "string"
1620 |                     type: "array"
1621 |                 type: "object"
1622 |           description: "Success"
1623 |         "404":
1624 |           content:
1625 |             application/json:
1626 |               schema:
1627 |                 "$ref": "#/components/schemas/Error"
1628 |           description: "Directory does not exist"
1629 |       summary: |
1630 |         List files that exist in the specified directory.
1631 |       tags:
1632 |         - "Vault Directories"
1633 | security:
1634 |   - apiKeyAuth: []
1635 | servers:
1636 |   - description: "HTTPS (Secure Mode)"
1637 |     url: "https://{host}:{port}"
1638 |     variables:
1639 |       host:
1640 |         default: "127.0.0.1"
1641 |         description: "Binding host"
1642 |       port:
1643 |         default: "27124"
1644 |         description: "HTTPS port"
1645 |   - description: "HTTP (Insecure Mode)"
1646 |     url: "http://{host}:{port}"
1647 |     variables:
1648 |       host:
1649 |         default: "127.0.0.1"
1650 |         description: "Binding host"
1651 |       port:
1652 |         default: "27123"
1653 |         description: "HTTP port"
1654 | 
```
Page 2/2FirstPrevNextLast