feat(obsidian): add Obsidian integration with 15 tools (#3455)

* feat(obsidian): add Obsidian integration with 15 tools

* fix(obsidian): encode path segments individually to preserve slashes

* improvement(obsidian): add type re-exports and improve output descriptions

* fix(obsidian): remove unreachable 404 handling from transformResponse
This commit is contained in:
Waleed
2026-03-06 23:13:47 -08:00
committed by GitHub
parent 0a52b09deb
commit 53fd92a30a
25 changed files with 2293 additions and 0 deletions
+149
View File
@@ -710,6 +710,155 @@ export function PerplexityIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function ObsidianIcon(props: SVGProps<SVGSVGElement>) {
const id = useId()
const bl = `${id}-bl`
const tr = `${id}-tr`
const tl = `${id}-tl`
const br = `${id}-br`
const te = `${id}-te`
const le = `${id}-le`
const be = `${id}-be`
const me = `${id}-me`
const clip = `${id}-clip`
return (
<svg {...props} viewBox='0 0 512 512' fill='none' xmlns='http://www.w3.org/2000/svg'>
<radialGradient
id={bl}
cx='0'
cy='0'
gradientTransform='matrix(-59 -225 150 -39 161.4 470)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.4' />
<stop offset='1' stopOpacity='.1' />
</radialGradient>
<radialGradient
id={tr}
cx='0'
cy='0'
gradientTransform='matrix(50 -379 280 37 360 374.2)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.6' />
<stop offset='1' stopColor='#fff' stopOpacity='.1' />
</radialGradient>
<radialGradient
id={tl}
cx='0'
cy='0'
gradientTransform='matrix(69 -319 218 47 175.4 307)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.8' />
<stop offset='1' stopColor='#fff' stopOpacity='.4' />
</radialGradient>
<radialGradient
id={br}
cx='0'
cy='0'
gradientTransform='matrix(-96 -163 187 -111 335.3 512.2)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.3' />
<stop offset='1' stopOpacity='.3' />
</radialGradient>
<radialGradient
id={te}
cx='0'
cy='0'
gradientTransform='matrix(-36 166 -112 -24 310 128.2)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='0' />
<stop offset='1' stopColor='#fff' stopOpacity='.2' />
</radialGradient>
<radialGradient
id={le}
cx='0'
cy='0'
gradientTransform='matrix(88 89 -190 187 111 220.2)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.2' />
<stop offset='1' stopColor='#fff' stopOpacity='.4' />
</radialGradient>
<radialGradient
id={be}
cx='0'
cy='0'
gradientTransform='matrix(9 130 -276 20 215 284)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.2' />
<stop offset='1' stopColor='#fff' stopOpacity='.3' />
</radialGradient>
<radialGradient
id={me}
cx='0'
cy='0'
gradientTransform='matrix(-198 -104 327 -623 400 399.2)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.2' />
<stop offset='.5' stopColor='#fff' stopOpacity='.2' />
<stop offset='1' stopColor='#fff' stopOpacity='.3' />
</radialGradient>
<clipPath id={clip}>
<path d='M.2.2h512v512H.2z' />
</clipPath>
<g clipPath={`url(#${clip})`}>
<path
d='M382.3 475.6c-3.1 23.4-26 41.6-48.7 35.3-32.4-8.9-69.9-22.8-103.6-25.4l-51.7-4a34 34 0 0 1-22-10.2l-89-91.7a34 34 0 0 1-6.7-37.7s55-121 57.1-127.3c2-6.3 9.6-61.2 14-90.6 1.2-7.9 5-15 11-20.3L248 8.9a34.1 34.1 0 0 1 49.6 4.3L386 125.6a37 37 0 0 1 7.6 22.4c0 21.3 1.8 65 13.6 93.2 11.5 27.3 32.5 57 43.5 71.5a17.3 17.3 0 0 1 1.3 19.2 1494 1494 0 0 1-44.8 70.6c-15 22.3-21.9 49.9-25 73.1z'
fill='#6c31e3'
/>
<path
d='M165.9 478.3c41.4-84 40.2-144.2 22.6-187-16.2-39.6-46.3-64.5-70-80-.6 2.3-1.3 4.4-2.2 6.5L60.6 342a34 34 0 0 0 6.6 37.7l89.1 91.7a34 34 0 0 0 9.6 7z'
fill={`url(#${bl})`}
/>
<path
d='M278.4 307.8c11.2 1.2 22.2 3.6 32.8 7.6 34 12.7 65 41.2 90.5 96.3 1.8-3.1 3.6-6.2 5.6-9.2a1536 1536 0 0 0 44.8-70.6 17 17 0 0 0-1.3-19.2c-11-14.6-32-44.2-43.5-71.5-11.8-28.2-13.5-72-13.6-93.2 0-8.1-2.6-16-7.6-22.4L297.6 13.2a34 34 0 0 0-1.5-1.7 96 96 0 0 1 2 54 198.3 198.3 0 0 1-17.6 41.3l-7.2 14.2a171 171 0 0 0-19.4 71c-1.2 29.4 4.8 66.4 24.5 115.8z'
fill={`url(#${tr})`}
/>
<path
d='M278.4 307.8c-19.7-49.4-25.8-86.4-24.5-115.9a171 171 0 0 1 19.4-71c2.3-4.8 4.8-9.5 7.2-14.1 7.1-13.9 14-27 17.6-41.4a96 96 0 0 0-2-54A34.1 34.1 0 0 0 248 9l-105.4 94.8a34.1 34.1 0 0 0-10.9 20.3l-12.8 85-.5 2.3c23.8 15.5 54 40.4 70.1 80a147 147 0 0 1 7.8 24.8c28-6.8 55.7-11 82.1-8.3z'
fill={`url(#${tl})`}
/>
<path
d='M333.6 511c22.7 6.2 45.6-12 48.7-35.4a187 187 0 0 1 19.4-63.9c-25.6-55-56.5-83.6-90.4-96.3-36-13.4-75.2-9-115 .7 8.9 40.4 3.6 93.3-30.4 162.2 4 1.8 8.1 3 12.5 3.3 0 0 24.4 2 53.6 4.1 29 2 72.4 17.1 101.6 25.2z'
fill={`url(#${br})`}
/>
<g clipRule='evenodd' fillRule='evenodd'>
<path
d='M254.1 190c-1.3 29.2 2.4 62.8 22.1 112.1l-6.2-.5c-17.7-51.5-21.5-78-20.2-107.6a174.7 174.7 0 0 1 20.4-72c2.4-4.9 8-14.1 10.5-18.8 7.1-13.7 11.9-21 16-33.6 5.7-17.5 4.5-25.9 3.8-34.1 4.6 29.9-12.7 56-25.7 82.4a177.1 177.1 0 0 0-20.7 72z'
fill={`url(#${te})`}
/>
<path
d='M194.3 293.4c2.4 5.4 4.6 9.8 6 16.5L195 311c-2.1-7.8-3.8-13.4-6.8-20-17.8-42-46.3-63.6-69.7-79.5 28.2 15.2 57.2 39 75.7 81.9z'
fill={`url(#${le})`}
/>
<path
d='M200.6 315.1c9.8 46-1.2 104.2-33.6 160.9 27.1-56.2 40.2-110.1 29.3-160z'
fill={`url(#${be})`}
/>
<path
d='M312.5 311c53.1 19.9 73.6 63.6 88.9 100-19-38.1-45.2-80.3-90.8-96-34.8-11.8-64.1-10.4-114.3 1l-1.1-5c53.2-12.1 81-13.5 117.3 0z'
fill={`url(#${me})`}
/>
</g>
</g>
</svg>
)
}
export function NotionIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 50 50' width='1em' height='1em' {...props}>
+2
View File
@@ -103,6 +103,7 @@ import {
MySQLIcon,
Neo4jIcon,
NotionIcon,
ObsidianIcon,
OnePasswordIcon,
OpenAIIcon,
OutlookIcon,
@@ -265,6 +266,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
mysql: MySQLIcon,
neo4j: Neo4jIcon,
notion_v2: NotionIcon,
obsidian: ObsidianIcon,
onedrive: MicrosoftOneDriveIcon,
onepassword: OnePasswordIcon,
openai: OpenAIIcon,
@@ -98,6 +98,7 @@
"mysql",
"neo4j",
"notion",
"obsidian",
"onedrive",
"onepassword",
"openai",
@@ -0,0 +1,323 @@
---
title: Obsidian
description: Interact with your Obsidian vault via the Local REST API
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="obsidian"
color="#0F0F0F"
/>
## Usage Instructions
Read, create, update, search, and delete notes in your Obsidian vault. Manage periodic notes, execute commands, and patch content at specific locations. Requires the Obsidian Local REST API plugin.
## Tools
### `obsidian_append_active`
Append content to the currently active file in Obsidian
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `content` | string | Yes | Markdown content to append to the active file |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `appended` | boolean | Whether content was successfully appended |
### `obsidian_append_note`
Append content to an existing note in your Obsidian vault
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `filename` | string | Yes | Path to the note relative to vault root \(e.g. "folder/note.md"\) |
| `content` | string | Yes | Markdown content to append to the note |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `filename` | string | Path of the note |
| `appended` | boolean | Whether content was successfully appended |
### `obsidian_append_periodic_note`
Append content to the current periodic note (daily, weekly, monthly, quarterly, or yearly). Creates the note if it does not exist.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `period` | string | Yes | Period type: daily, weekly, monthly, quarterly, or yearly |
| `content` | string | Yes | Markdown content to append to the periodic note |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `period` | string | Period type of the note |
| `appended` | boolean | Whether content was successfully appended |
### `obsidian_create_note`
Create or replace a note in your Obsidian vault
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `filename` | string | Yes | Path for the note relative to vault root \(e.g. "folder/note.md"\) |
| `content` | string | Yes | Markdown content for the note |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `filename` | string | Path of the created note |
| `created` | boolean | Whether the note was successfully created |
### `obsidian_delete_note`
Delete a note from your Obsidian vault
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `filename` | string | Yes | Path to the note to delete relative to vault root |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `filename` | string | Path of the deleted note |
| `deleted` | boolean | Whether the note was successfully deleted |
### `obsidian_execute_command`
Execute a command in Obsidian (e.g. open daily note, toggle sidebar)
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `commandId` | string | Yes | ID of the command to execute \(use List Commands operation to discover available commands\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `commandId` | string | ID of the executed command |
| `executed` | boolean | Whether the command was successfully executed |
### `obsidian_get_active`
Retrieve the content of the currently active file in Obsidian
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Markdown content of the active file |
| `filename` | string | Path to the active file |
### `obsidian_get_note`
Retrieve the content of a note from your Obsidian vault
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `filename` | string | Yes | Path to the note relative to vault root \(e.g. "folder/note.md"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Markdown content of the note |
| `filename` | string | Path to the note |
### `obsidian_get_periodic_note`
Retrieve the current periodic note (daily, weekly, monthly, quarterly, or yearly)
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `period` | string | Yes | Period type: daily, weekly, monthly, quarterly, or yearly |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Markdown content of the periodic note |
| `period` | string | Period type of the note |
### `obsidian_list_commands`
List all available commands in Obsidian
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `commands` | json | List of available commands with IDs and names |
| ↳ `id` | string | Command identifier |
| ↳ `name` | string | Human-readable command name |
### `obsidian_list_files`
List files and directories in your Obsidian vault
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `path` | string | No | Directory path relative to vault root. Leave empty to list root. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `files` | json | List of files and directories |
| ↳ `path` | string | File or directory path |
| ↳ `type` | string | Whether the entry is a file or directory |
### `obsidian_open_file`
Open a file in the Obsidian UI (creates the file if it does not exist)
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `filename` | string | Yes | Path to the file relative to vault root |
| `newLeaf` | boolean | No | Whether to open the file in a new leaf/tab |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `filename` | string | Path of the opened file |
| `opened` | boolean | Whether the file was successfully opened |
### `obsidian_patch_active`
Insert or replace content at a specific heading, block reference, or frontmatter field in the active file
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `content` | string | Yes | Content to insert at the target location |
| `operation` | string | Yes | How to insert content: append, prepend, or replace |
| `targetType` | string | Yes | Type of target: heading, block, or frontmatter |
| `target` | string | Yes | Target identifier \(heading text, block reference ID, or frontmatter field name\) |
| `targetDelimiter` | string | No | Delimiter for nested headings \(default: "::"\) |
| `trimTargetWhitespace` | boolean | No | Whether to trim whitespace from target before matching \(default: false\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `patched` | boolean | Whether the active file was successfully patched |
### `obsidian_patch_note`
Insert or replace content at a specific heading, block reference, or frontmatter field in a note
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `filename` | string | Yes | Path to the note relative to vault root \(e.g. "folder/note.md"\) |
| `content` | string | Yes | Content to insert at the target location |
| `operation` | string | Yes | How to insert content: append, prepend, or replace |
| `targetType` | string | Yes | Type of target: heading, block, or frontmatter |
| `target` | string | Yes | Target identifier \(heading text, block reference ID, or frontmatter field name\) |
| `targetDelimiter` | string | No | Delimiter for nested headings \(default: "::"\) |
| `trimTargetWhitespace` | boolean | No | Whether to trim whitespace from target before matching \(default: false\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `filename` | string | Path of the patched note |
| `patched` | boolean | Whether the note was successfully patched |
### `obsidian_search`
Search for text across notes in your Obsidian vault
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `query` | string | Yes | Text to search for across vault notes |
| `contextLength` | number | No | Number of characters of context around each match \(default: 100\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `results` | json | Search results with filenames, scores, and matching contexts |
| ↳ `filename` | string | Path to the matching note |
| ↳ `score` | number | Relevance score |
| ↳ `matches` | json | Matching text contexts |
| ↳ `context` | string | Text surrounding the match |
+270
View File
@@ -0,0 +1,270 @@
import { ObsidianIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
export const ObsidianBlock: BlockConfig = {
type: 'obsidian',
name: 'Obsidian',
description: 'Interact with your Obsidian vault via the Local REST API',
longDescription:
'Read, create, update, search, and delete notes in your Obsidian vault. Manage periodic notes, execute commands, and patch content at specific locations. Requires the Obsidian Local REST API plugin.',
docsLink: 'https://docs.sim.ai/tools/obsidian',
category: 'tools',
bgColor: '#0F0F0F',
icon: ObsidianIcon,
authMode: AuthMode.ApiKey,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'List Files', id: 'list_files' },
{ label: 'Get Note', id: 'get_note' },
{ label: 'Create Note', id: 'create_note' },
{ label: 'Append to Note', id: 'append_note' },
{ label: 'Patch Note', id: 'patch_note' },
{ label: 'Delete Note', id: 'delete_note' },
{ label: 'Search', id: 'search' },
{ label: 'Get Active File', id: 'get_active' },
{ label: 'Append to Active File', id: 'append_active' },
{ label: 'Patch Active File', id: 'patch_active' },
{ label: 'Open File', id: 'open_file' },
{ label: 'List Commands', id: 'list_commands' },
{ label: 'Execute Command', id: 'execute_command' },
{ label: 'Get Periodic Note', id: 'get_periodic_note' },
{ label: 'Append to Periodic Note', id: 'append_periodic_note' },
],
value: () => 'get_note',
},
{
id: 'baseUrl',
title: 'Base URL',
type: 'short-input',
placeholder: 'https://127.0.0.1:27124',
value: () => 'https://127.0.0.1:27124',
required: true,
},
{
id: 'apiKey',
title: 'API Key',
type: 'short-input',
placeholder: 'Enter your Obsidian Local REST API key',
password: true,
required: true,
},
{
id: 'path',
title: 'Directory Path',
type: 'short-input',
placeholder: 'Leave empty for vault root (e.g. "Projects/notes")',
condition: { field: 'operation', value: 'list_files' },
},
{
id: 'filename',
title: 'Note Path',
type: 'short-input',
placeholder: 'folder/note.md',
condition: {
field: 'operation',
value: ['get_note', 'create_note', 'append_note', 'patch_note', 'delete_note', 'open_file'],
},
required: {
field: 'operation',
value: ['get_note', 'create_note', 'append_note', 'patch_note', 'delete_note', 'open_file'],
},
},
{
id: 'content',
title: 'Content',
type: 'long-input',
placeholder: 'Markdown content',
condition: {
field: 'operation',
value: [
'create_note',
'append_note',
'patch_note',
'append_active',
'patch_active',
'append_periodic_note',
],
},
required: {
field: 'operation',
value: [
'create_note',
'append_note',
'patch_note',
'append_active',
'patch_active',
'append_periodic_note',
],
},
},
{
id: 'patchOperation',
title: 'Patch Operation',
type: 'dropdown',
options: [
{ label: 'Append', id: 'append' },
{ label: 'Prepend', id: 'prepend' },
{ label: 'Replace', id: 'replace' },
],
value: () => 'append',
condition: { field: 'operation', value: ['patch_note', 'patch_active'] },
required: { field: 'operation', value: ['patch_note', 'patch_active'] },
},
{
id: 'targetType',
title: 'Target Type',
type: 'dropdown',
options: [
{ label: 'Heading', id: 'heading' },
{ label: 'Block Reference', id: 'block' },
{ label: 'Frontmatter', id: 'frontmatter' },
],
value: () => 'heading',
condition: { field: 'operation', value: ['patch_note', 'patch_active'] },
required: { field: 'operation', value: ['patch_note', 'patch_active'] },
},
{
id: 'target',
title: 'Target',
type: 'short-input',
placeholder: 'Heading text, block ID, or frontmatter field',
condition: { field: 'operation', value: ['patch_note', 'patch_active'] },
required: { field: 'operation', value: ['patch_note', 'patch_active'] },
},
{
id: 'targetDelimiter',
title: 'Target Delimiter',
type: 'short-input',
placeholder: ':: (default)',
condition: { field: 'operation', value: ['patch_note', 'patch_active'] },
mode: 'advanced',
},
{
id: 'trimTargetWhitespace',
title: 'Trim Target Whitespace',
type: 'switch',
condition: { field: 'operation', value: ['patch_note', 'patch_active'] },
mode: 'advanced',
},
{
id: 'query',
title: 'Search Query',
type: 'short-input',
placeholder: 'Text to search for',
condition: { field: 'operation', value: 'search' },
required: { field: 'operation', value: 'search' },
},
{
id: 'contextLength',
title: 'Context Length',
type: 'short-input',
placeholder: '100',
condition: { field: 'operation', value: 'search' },
mode: 'advanced',
},
{
id: 'commandId',
title: 'Command ID',
type: 'short-input',
placeholder: 'e.g. daily-notes:open-today',
condition: { field: 'operation', value: 'execute_command' },
required: { field: 'operation', value: 'execute_command' },
},
{
id: 'newLeaf',
title: 'Open in New Tab',
type: 'switch',
condition: { field: 'operation', value: 'open_file' },
mode: 'advanced',
},
{
id: 'period',
title: 'Period',
type: 'dropdown',
options: [
{ label: 'Daily', id: 'daily' },
{ label: 'Weekly', id: 'weekly' },
{ label: 'Monthly', id: 'monthly' },
{ label: 'Quarterly', id: 'quarterly' },
{ label: 'Yearly', id: 'yearly' },
],
value: () => 'daily',
condition: { field: 'operation', value: ['get_periodic_note', 'append_periodic_note'] },
required: { field: 'operation', value: ['get_periodic_note', 'append_periodic_note'] },
},
],
tools: {
access: [
'obsidian_append_active',
'obsidian_append_note',
'obsidian_append_periodic_note',
'obsidian_create_note',
'obsidian_delete_note',
'obsidian_execute_command',
'obsidian_get_active',
'obsidian_get_note',
'obsidian_get_periodic_note',
'obsidian_list_commands',
'obsidian_list_files',
'obsidian_open_file',
'obsidian_patch_active',
'obsidian_patch_note',
'obsidian_search',
],
config: {
tool: (params) => `obsidian_${params.operation}`,
params: (params) => {
const result: Record<string, unknown> = {}
if (params.contextLength) {
result.contextLength = Number(params.contextLength)
}
if (params.patchOperation) {
result.operation = params.patchOperation
}
return result
},
},
},
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
baseUrl: { type: 'string', description: 'Base URL for the Obsidian Local REST API' },
apiKey: { type: 'string', description: 'API key for authentication' },
filename: { type: 'string', description: 'Path to the note relative to vault root' },
content: { type: 'string', description: 'Markdown content for the note' },
path: { type: 'string', description: 'Directory path to list' },
query: { type: 'string', description: 'Text to search for' },
contextLength: { type: 'number', description: 'Characters of context around matches' },
commandId: { type: 'string', description: 'ID of the command to execute' },
patchOperation: { type: 'string', description: 'Patch operation: append, prepend, or replace' },
targetType: { type: 'string', description: 'Target type: heading, block, or frontmatter' },
target: { type: 'string', description: 'Target identifier for patch operations' },
targetDelimiter: { type: 'string', description: 'Delimiter for nested headings' },
trimTargetWhitespace: { type: 'boolean', description: 'Trim whitespace from target' },
newLeaf: { type: 'boolean', description: 'Open file in new tab' },
period: { type: 'string', description: 'Periodic note period type' },
},
outputs: {
content: { type: 'string', description: 'Markdown content of the note' },
filename: { type: 'string', description: 'Path to the note' },
files: { type: 'json', description: 'List of files and directories (path, type)' },
results: { type: 'json', description: 'Search results (filename, score, matches)' },
commands: { type: 'json', description: 'List of available commands (id, name)' },
created: { type: 'boolean', description: 'Whether the note was created' },
appended: { type: 'boolean', description: 'Whether content was appended' },
patched: { type: 'boolean', description: 'Whether content was patched' },
deleted: { type: 'boolean', description: 'Whether the note was deleted' },
executed: { type: 'boolean', description: 'Whether the command was executed' },
opened: { type: 'boolean', description: 'Whether the file was opened' },
commandId: { type: 'string', description: 'ID of the executed command' },
period: { type: 'string', description: 'Period type of the periodic note' },
},
}
+2
View File
@@ -113,6 +113,7 @@ import { MySQLBlock } from '@/blocks/blocks/mysql'
import { Neo4jBlock } from '@/blocks/blocks/neo4j'
import { NoteBlock } from '@/blocks/blocks/note'
import { NotionBlock, NotionV2Block } from '@/blocks/blocks/notion'
import { ObsidianBlock } from '@/blocks/blocks/obsidian'
import { OneDriveBlock } from '@/blocks/blocks/onedrive'
import { OnePasswordBlock } from '@/blocks/blocks/onepassword'
import { OpenAIBlock } from '@/blocks/blocks/openai'
@@ -320,6 +321,7 @@ export const registry: Record<string, BlockConfig> = {
note: NoteBlock,
notion: NotionBlock,
notion_v2: NotionV2Block,
obsidian: ObsidianBlock,
onepassword: OnePasswordBlock,
onedrive: OneDriveBlock,
openai: OpenAIBlock,
+149
View File
@@ -710,6 +710,155 @@ export function PerplexityIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function ObsidianIcon(props: SVGProps<SVGSVGElement>) {
const id = useId()
const bl = `${id}-bl`
const tr = `${id}-tr`
const tl = `${id}-tl`
const br = `${id}-br`
const te = `${id}-te`
const le = `${id}-le`
const be = `${id}-be`
const me = `${id}-me`
const clip = `${id}-clip`
return (
<svg {...props} viewBox='0 0 512 512' fill='none' xmlns='http://www.w3.org/2000/svg'>
<radialGradient
id={bl}
cx='0'
cy='0'
gradientTransform='matrix(-59 -225 150 -39 161.4 470)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.4' />
<stop offset='1' stopOpacity='.1' />
</radialGradient>
<radialGradient
id={tr}
cx='0'
cy='0'
gradientTransform='matrix(50 -379 280 37 360 374.2)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.6' />
<stop offset='1' stopColor='#fff' stopOpacity='.1' />
</radialGradient>
<radialGradient
id={tl}
cx='0'
cy='0'
gradientTransform='matrix(69 -319 218 47 175.4 307)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.8' />
<stop offset='1' stopColor='#fff' stopOpacity='.4' />
</radialGradient>
<radialGradient
id={br}
cx='0'
cy='0'
gradientTransform='matrix(-96 -163 187 -111 335.3 512.2)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.3' />
<stop offset='1' stopOpacity='.3' />
</radialGradient>
<radialGradient
id={te}
cx='0'
cy='0'
gradientTransform='matrix(-36 166 -112 -24 310 128.2)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='0' />
<stop offset='1' stopColor='#fff' stopOpacity='.2' />
</radialGradient>
<radialGradient
id={le}
cx='0'
cy='0'
gradientTransform='matrix(88 89 -190 187 111 220.2)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.2' />
<stop offset='1' stopColor='#fff' stopOpacity='.4' />
</radialGradient>
<radialGradient
id={be}
cx='0'
cy='0'
gradientTransform='matrix(9 130 -276 20 215 284)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.2' />
<stop offset='1' stopColor='#fff' stopOpacity='.3' />
</radialGradient>
<radialGradient
id={me}
cx='0'
cy='0'
gradientTransform='matrix(-198 -104 327 -623 400 399.2)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.2' />
<stop offset='.5' stopColor='#fff' stopOpacity='.2' />
<stop offset='1' stopColor='#fff' stopOpacity='.3' />
</radialGradient>
<clipPath id={clip}>
<path d='M.2.2h512v512H.2z' />
</clipPath>
<g clipPath={`url(#${clip})`}>
<path
d='M382.3 475.6c-3.1 23.4-26 41.6-48.7 35.3-32.4-8.9-69.9-22.8-103.6-25.4l-51.7-4a34 34 0 0 1-22-10.2l-89-91.7a34 34 0 0 1-6.7-37.7s55-121 57.1-127.3c2-6.3 9.6-61.2 14-90.6 1.2-7.9 5-15 11-20.3L248 8.9a34.1 34.1 0 0 1 49.6 4.3L386 125.6a37 37 0 0 1 7.6 22.4c0 21.3 1.8 65 13.6 93.2 11.5 27.3 32.5 57 43.5 71.5a17.3 17.3 0 0 1 1.3 19.2 1494 1494 0 0 1-44.8 70.6c-15 22.3-21.9 49.9-25 73.1z'
fill='#6c31e3'
/>
<path
d='M165.9 478.3c41.4-84 40.2-144.2 22.6-187-16.2-39.6-46.3-64.5-70-80-.6 2.3-1.3 4.4-2.2 6.5L60.6 342a34 34 0 0 0 6.6 37.7l89.1 91.7a34 34 0 0 0 9.6 7z'
fill={`url(#${bl})`}
/>
<path
d='M278.4 307.8c11.2 1.2 22.2 3.6 32.8 7.6 34 12.7 65 41.2 90.5 96.3 1.8-3.1 3.6-6.2 5.6-9.2a1536 1536 0 0 0 44.8-70.6 17 17 0 0 0-1.3-19.2c-11-14.6-32-44.2-43.5-71.5-11.8-28.2-13.5-72-13.6-93.2 0-8.1-2.6-16-7.6-22.4L297.6 13.2a34 34 0 0 0-1.5-1.7 96 96 0 0 1 2 54 198.3 198.3 0 0 1-17.6 41.3l-7.2 14.2a171 171 0 0 0-19.4 71c-1.2 29.4 4.8 66.4 24.5 115.8z'
fill={`url(#${tr})`}
/>
<path
d='M278.4 307.8c-19.7-49.4-25.8-86.4-24.5-115.9a171 171 0 0 1 19.4-71c2.3-4.8 4.8-9.5 7.2-14.1 7.1-13.9 14-27 17.6-41.4a96 96 0 0 0-2-54A34.1 34.1 0 0 0 248 9l-105.4 94.8a34.1 34.1 0 0 0-10.9 20.3l-12.8 85-.5 2.3c23.8 15.5 54 40.4 70.1 80a147 147 0 0 1 7.8 24.8c28-6.8 55.7-11 82.1-8.3z'
fill={`url(#${tl})`}
/>
<path
d='M333.6 511c22.7 6.2 45.6-12 48.7-35.4a187 187 0 0 1 19.4-63.9c-25.6-55-56.5-83.6-90.4-96.3-36-13.4-75.2-9-115 .7 8.9 40.4 3.6 93.3-30.4 162.2 4 1.8 8.1 3 12.5 3.3 0 0 24.4 2 53.6 4.1 29 2 72.4 17.1 101.6 25.2z'
fill={`url(#${br})`}
/>
<g clipRule='evenodd' fillRule='evenodd'>
<path
d='M254.1 190c-1.3 29.2 2.4 62.8 22.1 112.1l-6.2-.5c-17.7-51.5-21.5-78-20.2-107.6a174.7 174.7 0 0 1 20.4-72c2.4-4.9 8-14.1 10.5-18.8 7.1-13.7 11.9-21 16-33.6 5.7-17.5 4.5-25.9 3.8-34.1 4.6 29.9-12.7 56-25.7 82.4a177.1 177.1 0 0 0-20.7 72z'
fill={`url(#${te})`}
/>
<path
d='M194.3 293.4c2.4 5.4 4.6 9.8 6 16.5L195 311c-2.1-7.8-3.8-13.4-6.8-20-17.8-42-46.3-63.6-69.7-79.5 28.2 15.2 57.2 39 75.7 81.9z'
fill={`url(#${le})`}
/>
<path
d='M200.6 315.1c9.8 46-1.2 104.2-33.6 160.9 27.1-56.2 40.2-110.1 29.3-160z'
fill={`url(#${be})`}
/>
<path
d='M312.5 311c53.1 19.9 73.6 63.6 88.9 100-19-38.1-45.2-80.3-90.8-96-34.8-11.8-64.1-10.4-114.3 1l-1.1-5c53.2-12.1 81-13.5 117.3 0z'
fill={`url(#${me})`}
/>
</g>
</g>
</svg>
)
}
export function NotionIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 50 50' width='1em' height='1em' {...props}>
+66
View File
@@ -0,0 +1,66 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianAppendActiveParams, ObsidianAppendActiveResponse } from './types'
export const appendActiveTool: ToolConfig<
ObsidianAppendActiveParams,
ObsidianAppendActiveResponse
> = {
id: 'obsidian_append_active',
name: 'Obsidian Append to Active File',
description: 'Append content to the currently active file in Obsidian',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
content: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Markdown content to append to the active file',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
return `${base}/active/`
},
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'text/markdown',
}),
body: (params) => params.content,
},
transformResponse: async (response) => {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
throw new Error(`Failed to append to active file: ${error.message ?? response.statusText}`)
}
return {
success: true,
output: {
appended: true,
},
}
},
outputs: {
appended: {
type: 'boolean',
description: 'Whether content was successfully appended',
},
},
}
+74
View File
@@ -0,0 +1,74 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianAppendNoteParams, ObsidianAppendNoteResponse } from './types'
export const appendNoteTool: ToolConfig<ObsidianAppendNoteParams, ObsidianAppendNoteResponse> = {
id: 'obsidian_append_note',
name: 'Obsidian Append to Note',
description: 'Append content to an existing note in your Obsidian vault',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
filename: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Path to the note relative to vault root (e.g. "folder/note.md")',
},
content: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Markdown content to append to the note',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
return `${base}/vault/${params.filename.trim().split('/').map(encodeURIComponent).join('/')}`
},
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'text/markdown',
}),
body: (params) => params.content,
},
transformResponse: async (response, params) => {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
throw new Error(`Failed to append to note: ${error.message ?? response.statusText}`)
}
return {
success: true,
output: {
filename: params?.filename ?? '',
appended: true,
},
}
},
outputs: {
filename: {
type: 'string',
description: 'Path of the note',
},
appended: {
type: 'boolean',
description: 'Whether content was successfully appended',
},
},
}
@@ -0,0 +1,78 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianAppendPeriodicNoteParams, ObsidianAppendPeriodicNoteResponse } from './types'
export const appendPeriodicNoteTool: ToolConfig<
ObsidianAppendPeriodicNoteParams,
ObsidianAppendPeriodicNoteResponse
> = {
id: 'obsidian_append_periodic_note',
name: 'Obsidian Append to Periodic Note',
description:
'Append content to the current periodic note (daily, weekly, monthly, quarterly, or yearly). Creates the note if it does not exist.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
period: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Period type: daily, weekly, monthly, quarterly, or yearly',
},
content: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Markdown content to append to the periodic note',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
return `${base}/periodic/${encodeURIComponent(params.period)}/`
},
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'text/markdown',
}),
body: (params) => params.content,
},
transformResponse: async (response, params) => {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
throw new Error(`Failed to append to periodic note: ${error.message ?? response.statusText}`)
}
return {
success: true,
output: {
period: params?.period ?? '',
appended: true,
},
}
},
outputs: {
period: {
type: 'string',
description: 'Period type of the note',
},
appended: {
type: 'boolean',
description: 'Whether content was successfully appended',
},
},
}
+74
View File
@@ -0,0 +1,74 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianCreateNoteParams, ObsidianCreateNoteResponse } from './types'
export const createNoteTool: ToolConfig<ObsidianCreateNoteParams, ObsidianCreateNoteResponse> = {
id: 'obsidian_create_note',
name: 'Obsidian Create Note',
description: 'Create or replace a note in your Obsidian vault',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
filename: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Path for the note relative to vault root (e.g. "folder/note.md")',
},
content: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Markdown content for the note',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
return `${base}/vault/${params.filename.trim().split('/').map(encodeURIComponent).join('/')}`
},
method: 'PUT',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'text/markdown',
}),
body: (params) => params.content,
},
transformResponse: async (response, params) => {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
throw new Error(`Failed to create note: ${error.message ?? response.statusText}`)
}
return {
success: true,
output: {
filename: params?.filename ?? '',
created: true,
},
}
},
outputs: {
filename: {
type: 'string',
description: 'Path of the created note',
},
created: {
type: 'boolean',
description: 'Whether the note was successfully created',
},
},
}
+66
View File
@@ -0,0 +1,66 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianDeleteNoteParams, ObsidianDeleteNoteResponse } from './types'
export const deleteNoteTool: ToolConfig<ObsidianDeleteNoteParams, ObsidianDeleteNoteResponse> = {
id: 'obsidian_delete_note',
name: 'Obsidian Delete Note',
description: 'Delete a note from your Obsidian vault',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
filename: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Path to the note to delete relative to vault root',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
return `${base}/vault/${params.filename.trim().split('/').map(encodeURIComponent).join('/')}`
},
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response, params) => {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
throw new Error(`Failed to delete note: ${error.message ?? response.statusText}`)
}
return {
success: true,
output: {
filename: params?.filename ?? '',
deleted: true,
},
}
},
outputs: {
filename: {
type: 'string',
description: 'Path of the deleted note',
},
deleted: {
type: 'boolean',
description: 'Whether the note was successfully deleted',
},
},
}
@@ -0,0 +1,70 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianExecuteCommandParams, ObsidianExecuteCommandResponse } from './types'
export const executeCommandTool: ToolConfig<
ObsidianExecuteCommandParams,
ObsidianExecuteCommandResponse
> = {
id: 'obsidian_execute_command',
name: 'Obsidian Execute Command',
description: 'Execute a command in Obsidian (e.g. open daily note, toggle sidebar)',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
commandId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'ID of the command to execute (use List Commands operation to discover available commands)',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
return `${base}/commands/${encodeURIComponent(params.commandId.trim())}/`
},
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response, params) => {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
throw new Error(`Failed to execute command: ${error.message ?? response.statusText}`)
}
return {
success: true,
output: {
commandId: params?.commandId ?? '',
executed: true,
},
}
},
outputs: {
commandId: {
type: 'string',
description: 'ID of the executed command',
},
executed: {
type: 'boolean',
description: 'Whether the command was successfully executed',
},
},
}
+59
View File
@@ -0,0 +1,59 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianGetActiveParams, ObsidianGetActiveResponse } from './types'
export const getActiveTool: ToolConfig<ObsidianGetActiveParams, ObsidianGetActiveResponse> = {
id: 'obsidian_get_active',
name: 'Obsidian Get Active File',
description: 'Retrieve the content of the currently active file in Obsidian',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
return `${base}/active/`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
Accept: 'application/vnd.olrapi.note+json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
return {
success: true,
output: {
content: data.content ?? '',
filename: data.path ?? null,
},
}
},
outputs: {
content: {
type: 'string',
description: 'Markdown content of the active file',
},
filename: {
type: 'string',
description: 'Path to the active file',
optional: true,
},
},
}
+68
View File
@@ -0,0 +1,68 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianGetNoteParams, ObsidianGetNoteResponse } from './types'
export const getNoteTool: ToolConfig<ObsidianGetNoteParams, ObsidianGetNoteResponse> = {
id: 'obsidian_get_note',
name: 'Obsidian Get Note',
description: 'Retrieve the content of a note from your Obsidian vault',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
filename: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Path to the note relative to vault root (e.g. "folder/note.md")',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
return `${base}/vault/${params.filename.trim().split('/').map(encodeURIComponent).join('/')}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
Accept: 'text/markdown',
}),
},
transformResponse: async (response, params) => {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
throw new Error(`Failed to get note: ${error.message ?? response.statusText}`)
}
const content = await response.text()
return {
success: true,
output: {
content,
filename: params?.filename ?? '',
},
}
},
outputs: {
content: {
type: 'string',
description: 'Markdown content of the note',
},
filename: {
type: 'string',
description: 'Path to the note',
},
},
}
@@ -0,0 +1,67 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianGetPeriodicNoteParams, ObsidianGetPeriodicNoteResponse } from './types'
export const getPeriodicNoteTool: ToolConfig<
ObsidianGetPeriodicNoteParams,
ObsidianGetPeriodicNoteResponse
> = {
id: 'obsidian_get_periodic_note',
name: 'Obsidian Get Periodic Note',
description: 'Retrieve the current periodic note (daily, weekly, monthly, quarterly, or yearly)',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
period: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Period type: daily, weekly, monthly, quarterly, or yearly',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
return `${base}/periodic/${encodeURIComponent(params.period)}/`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
Accept: 'text/markdown',
}),
},
transformResponse: async (response, params) => {
const content = await response.text()
return {
success: true,
output: {
content,
period: params?.period ?? '',
},
}
},
outputs: {
content: {
type: 'string',
description: 'Markdown content of the periodic note',
},
period: {
type: 'string',
description: 'Period type of the note',
},
},
}
+16
View File
@@ -0,0 +1,16 @@
export { appendActiveTool as obsidianAppendActiveTool } from './append_active'
export { appendNoteTool as obsidianAppendNoteTool } from './append_note'
export { appendPeriodicNoteTool as obsidianAppendPeriodicNoteTool } from './append_periodic_note'
export { createNoteTool as obsidianCreateNoteTool } from './create_note'
export { deleteNoteTool as obsidianDeleteNoteTool } from './delete_note'
export { executeCommandTool as obsidianExecuteCommandTool } from './execute_command'
export { getActiveTool as obsidianGetActiveTool } from './get_active'
export { getNoteTool as obsidianGetNoteTool } from './get_note'
export { getPeriodicNoteTool as obsidianGetPeriodicNoteTool } from './get_periodic_note'
export { listCommandsTool as obsidianListCommandsTool } from './list_commands'
export { listFilesTool as obsidianListFilesTool } from './list_files'
export { openFileTool as obsidianOpenFileTool } from './open_file'
export { patchActiveTool as obsidianPatchActiveTool } from './patch_active'
export { patchNoteTool as obsidianPatchNoteTool } from './patch_note'
export { searchTool as obsidianSearchTool } from './search'
export * from './types'
+68
View File
@@ -0,0 +1,68 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianListCommandsParams, ObsidianListCommandsResponse } from './types'
export const listCommandsTool: ToolConfig<
ObsidianListCommandsParams,
ObsidianListCommandsResponse
> = {
id: 'obsidian_list_commands',
name: 'Obsidian List Commands',
description: 'List all available commands in Obsidian',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
return `${base}/commands/`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
Accept: 'application/json',
}),
},
transformResponse: async (response) => {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
throw new Error(`Failed to list commands: ${error.message ?? response.statusText}`)
}
const data = await response.json()
return {
success: true,
output: {
commands:
data.commands?.map((cmd: { id: string; name: string }) => ({
id: cmd.id ?? '',
name: cmd.name ?? '',
})) ?? [],
},
}
},
outputs: {
commands: {
type: 'json',
description: 'List of available commands with IDs and names',
properties: {
id: { type: 'string', description: 'Command identifier' },
name: { type: 'string', description: 'Human-readable command name' },
},
},
},
}
+76
View File
@@ -0,0 +1,76 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianListFilesParams, ObsidianListFilesResponse } from './types'
export const listFilesTool: ToolConfig<ObsidianListFilesParams, ObsidianListFilesResponse> = {
id: 'obsidian_list_files',
name: 'Obsidian List Files',
description: 'List files and directories in your Obsidian vault',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
path: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Directory path relative to vault root. Leave empty to list root.',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
const path = params.path
? `/${params.path.trim().split('/').map(encodeURIComponent).join('/')}/`
: '/'
return `${base}/vault${path}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
Accept: 'application/json',
}),
},
transformResponse: async (response) => {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
throw new Error(`Failed to list files: ${error.message ?? response.statusText}`)
}
const data = await response.json()
return {
success: true,
output: {
files:
data.files?.map((f: string | { path: string; type: string }) => {
if (typeof f === 'string') {
return { path: f, type: f.endsWith('/') ? 'directory' : 'file' }
}
return { path: f.path ?? '', type: f.type ?? 'file' }
}) ?? [],
},
}
},
outputs: {
files: {
type: 'json',
description: 'List of files and directories',
properties: {
path: { type: 'string', description: 'File or directory path' },
type: { type: 'string', description: 'Whether the entry is a file or directory' },
},
},
},
}
+73
View File
@@ -0,0 +1,73 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianOpenFileParams, ObsidianOpenFileResponse } from './types'
export const openFileTool: ToolConfig<ObsidianOpenFileParams, ObsidianOpenFileResponse> = {
id: 'obsidian_open_file',
name: 'Obsidian Open File',
description: 'Open a file in the Obsidian UI (creates the file if it does not exist)',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
filename: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Path to the file relative to vault root',
},
newLeaf: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to open the file in a new leaf/tab',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
const leafParam = params.newLeaf ? '?newLeaf=true' : ''
return `${base}/open/${params.filename.trim().split('/').map(encodeURIComponent).join('/')}${leafParam}`
},
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response, params) => {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
throw new Error(`Failed to open file: ${error.message ?? response.statusText}`)
}
return {
success: true,
output: {
filename: params?.filename ?? '',
opened: true,
},
}
},
outputs: {
filename: {
type: 'string',
description: 'Path of the opened file',
},
opened: {
type: 'boolean',
description: 'Whether the file was successfully opened',
},
},
}
+107
View File
@@ -0,0 +1,107 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianPatchActiveParams, ObsidianPatchActiveResponse } from './types'
export const patchActiveTool: ToolConfig<ObsidianPatchActiveParams, ObsidianPatchActiveResponse> = {
id: 'obsidian_patch_active',
name: 'Obsidian Patch Active File',
description:
'Insert or replace content at a specific heading, block reference, or frontmatter field in the active file',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
content: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Content to insert at the target location',
},
operation: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'How to insert content: append, prepend, or replace',
},
targetType: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Type of target: heading, block, or frontmatter',
},
target: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Target identifier (heading text, block reference ID, or frontmatter field name)',
},
targetDelimiter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Delimiter for nested headings (default: "::")',
},
trimTargetWhitespace: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to trim whitespace from target before matching (default: false)',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
return `${base}/active/`
},
method: 'PATCH',
headers: (params) => {
const headers: Record<string, string> = {
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'text/markdown',
Operation: params.operation,
'Target-Type': params.targetType,
Target: encodeURIComponent(params.target),
}
if (params.targetDelimiter) {
headers['Target-Delimiter'] = params.targetDelimiter
}
if (params.trimTargetWhitespace) {
headers['Trim-Target-Whitespace'] = 'true'
}
return headers
},
body: (params) => params.content,
},
transformResponse: async (response) => {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
throw new Error(`Failed to patch active file: ${error.message ?? response.statusText}`)
}
return {
success: true,
output: {
patched: true,
},
}
},
outputs: {
patched: {
type: 'boolean',
description: 'Whether the active file was successfully patched',
},
},
}
+118
View File
@@ -0,0 +1,118 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianPatchNoteParams, ObsidianPatchNoteResponse } from './types'
export const patchNoteTool: ToolConfig<ObsidianPatchNoteParams, ObsidianPatchNoteResponse> = {
id: 'obsidian_patch_note',
name: 'Obsidian Patch Note',
description:
'Insert or replace content at a specific heading, block reference, or frontmatter field in a note',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
filename: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Path to the note relative to vault root (e.g. "folder/note.md")',
},
content: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Content to insert at the target location',
},
operation: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'How to insert content: append, prepend, or replace',
},
targetType: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Type of target: heading, block, or frontmatter',
},
target: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Target identifier (heading text, block reference ID, or frontmatter field name)',
},
targetDelimiter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Delimiter for nested headings (default: "::")',
},
trimTargetWhitespace: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to trim whitespace from target before matching (default: false)',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
return `${base}/vault/${params.filename.trim().split('/').map(encodeURIComponent).join('/')}`
},
method: 'PATCH',
headers: (params) => {
const headers: Record<string, string> = {
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'text/markdown',
Operation: params.operation,
'Target-Type': params.targetType,
Target: encodeURIComponent(params.target),
}
if (params.targetDelimiter) {
headers['Target-Delimiter'] = params.targetDelimiter
}
if (params.trimTargetWhitespace) {
headers['Trim-Target-Whitespace'] = 'true'
}
return headers
},
body: (params) => params.content,
},
transformResponse: async (response, params) => {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
throw new Error(`Failed to patch note: ${error.message ?? response.statusText}`)
}
return {
success: true,
output: {
filename: params?.filename ?? '',
patched: true,
},
}
},
outputs: {
filename: {
type: 'string',
description: 'Path of the patched note',
},
patched: {
type: 'boolean',
description: 'Whether the note was successfully patched',
},
},
}
+95
View File
@@ -0,0 +1,95 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianSearchParams, ObsidianSearchResponse } from './types'
export const searchTool: ToolConfig<ObsidianSearchParams, ObsidianSearchResponse> = {
id: 'obsidian_search',
name: 'Obsidian Search',
description: 'Search for text across notes in your Obsidian vault',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
query: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Text to search for across vault notes',
},
contextLength: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of characters of context around each match (default: 100)',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
const contextParam = params.contextLength ? `&contextLength=${params.contextLength}` : ''
return `${base}/search/simple/?query=${encodeURIComponent(params.query)}${contextParam}`
},
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
Accept: 'application/json',
}),
},
transformResponse: async (response) => {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
throw new Error(`Search failed: ${error.message ?? response.statusText}`)
}
const data = await response.json()
return {
success: true,
output: {
results:
data?.map(
(item: {
filename: string
score: number
matches: Array<{ match: { start: number; end: number }; context: string }>
}) => ({
filename: item.filename ?? '',
score: item.score ?? 0,
matches:
item.matches?.map((m: { context: string }) => ({
context: m.context ?? '',
})) ?? [],
})
) ?? [],
},
}
},
outputs: {
results: {
type: 'json',
description: 'Search results with filenames, scores, and matching contexts',
properties: {
filename: { type: 'string', description: 'Path to the matching note' },
score: { type: 'number', description: 'Relevance score' },
matches: {
type: 'json',
description: 'Matching text contexts',
properties: {
context: { type: 'string', description: 'Text surrounding the match' },
},
},
},
},
},
}
+190
View File
@@ -0,0 +1,190 @@
import type { ToolResponse } from '@/tools/types'
export interface ObsidianBaseParams {
apiKey: string
baseUrl: string
}
export interface ObsidianListFilesParams extends ObsidianBaseParams {
path?: string
}
export interface ObsidianListFilesResponse extends ToolResponse {
output: {
files: Array<{
path: string
type: string
}>
}
}
export interface ObsidianGetNoteParams extends ObsidianBaseParams {
filename: string
}
export interface ObsidianGetNoteResponse extends ToolResponse {
output: {
content: string
filename: string
}
}
export interface ObsidianCreateNoteParams extends ObsidianBaseParams {
filename: string
content: string
}
export interface ObsidianCreateNoteResponse extends ToolResponse {
output: {
filename: string
created: boolean
}
}
export interface ObsidianAppendNoteParams extends ObsidianBaseParams {
filename: string
content: string
}
export interface ObsidianAppendNoteResponse extends ToolResponse {
output: {
filename: string
appended: boolean
}
}
export interface ObsidianPatchNoteParams extends ObsidianBaseParams {
filename: string
content: string
operation: string
targetType: string
target: string
targetDelimiter?: string
trimTargetWhitespace?: boolean
}
export interface ObsidianPatchNoteResponse extends ToolResponse {
output: {
filename: string
patched: boolean
}
}
export interface ObsidianDeleteNoteParams extends ObsidianBaseParams {
filename: string
}
export interface ObsidianDeleteNoteResponse extends ToolResponse {
output: {
filename: string
deleted: boolean
}
}
export interface ObsidianSearchParams extends ObsidianBaseParams {
query: string
contextLength?: number
}
export interface ObsidianSearchResponse extends ToolResponse {
output: {
results: Array<{
filename: string
score: number
matches: Array<{
context: string
}>
}>
}
}
export interface ObsidianGetActiveParams extends ObsidianBaseParams {}
export interface ObsidianGetActiveResponse extends ToolResponse {
output: {
content: string
filename: string | null
}
}
export interface ObsidianAppendActiveParams extends ObsidianBaseParams {
content: string
}
export interface ObsidianAppendActiveResponse extends ToolResponse {
output: {
appended: boolean
}
}
export interface ObsidianPatchActiveParams extends ObsidianBaseParams {
content: string
operation: string
targetType: string
target: string
targetDelimiter?: string
trimTargetWhitespace?: boolean
}
export interface ObsidianPatchActiveResponse extends ToolResponse {
output: {
patched: boolean
}
}
export interface ObsidianListCommandsParams extends ObsidianBaseParams {}
export interface ObsidianListCommandsResponse extends ToolResponse {
output: {
commands: Array<{
id: string
name: string
}>
}
}
export interface ObsidianExecuteCommandParams extends ObsidianBaseParams {
commandId: string
}
export interface ObsidianExecuteCommandResponse extends ToolResponse {
output: {
commandId: string
executed: boolean
}
}
export interface ObsidianOpenFileParams extends ObsidianBaseParams {
filename: string
newLeaf?: boolean
}
export interface ObsidianOpenFileResponse extends ToolResponse {
output: {
filename: string
opened: boolean
}
}
export interface ObsidianGetPeriodicNoteParams extends ObsidianBaseParams {
period: string
}
export interface ObsidianGetPeriodicNoteResponse extends ToolResponse {
output: {
content: string
period: string
}
}
export interface ObsidianAppendPeriodicNoteParams extends ObsidianBaseParams {
period: string
content: string
}
export interface ObsidianAppendPeriodicNoteResponse extends ToolResponse {
output: {
period: string
appended: boolean
}
}
+32
View File
@@ -1450,6 +1450,23 @@ import {
notionWriteTool,
notionWriteV2Tool,
} from '@/tools/notion'
import {
obsidianAppendActiveTool,
obsidianAppendNoteTool,
obsidianAppendPeriodicNoteTool,
obsidianCreateNoteTool,
obsidianDeleteNoteTool,
obsidianExecuteCommandTool,
obsidianGetActiveTool,
obsidianGetNoteTool,
obsidianGetPeriodicNoteTool,
obsidianListCommandsTool,
obsidianListFilesTool,
obsidianOpenFileTool,
obsidianPatchActiveTool,
obsidianPatchNoteTool,
obsidianSearchTool,
} from '@/tools/obsidian'
import {
onedriveCreateFolderTool,
onedriveDeleteTool,
@@ -2739,6 +2756,21 @@ export const tools: Record<string, ToolConfig> = {
notion_create_database_v2: notionCreateDatabaseV2Tool,
notion_update_page_v2: notionUpdatePageV2Tool,
notion_add_database_row_v2: notionAddDatabaseRowTool,
obsidian_append_active: obsidianAppendActiveTool,
obsidian_append_note: obsidianAppendNoteTool,
obsidian_append_periodic_note: obsidianAppendPeriodicNoteTool,
obsidian_create_note: obsidianCreateNoteTool,
obsidian_delete_note: obsidianDeleteNoteTool,
obsidian_execute_command: obsidianExecuteCommandTool,
obsidian_get_active: obsidianGetActiveTool,
obsidian_get_note: obsidianGetNoteTool,
obsidian_get_periodic_note: obsidianGetPeriodicNoteTool,
obsidian_list_commands: obsidianListCommandsTool,
obsidian_list_files: obsidianListFilesTool,
obsidian_open_file: obsidianOpenFileTool,
obsidian_patch_active: obsidianPatchActiveTool,
obsidian_patch_note: obsidianPatchNoteTool,
obsidian_search: obsidianSearchTool,
onepassword_list_vaults: onepasswordListVaultsTool,
onepassword_get_vault: onepasswordGetVaultTool,
onepassword_list_items: onepasswordListItemsTool,