diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 5525e048cfa..41fa14fa22a 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -710,6 +710,155 @@ export function PerplexityIcon(props: SVGProps) { ) } +export function ObsidianIcon(props: SVGProps) { + 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 ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + export function NotionIcon(props: SVGProps) { return ( diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 7b2ae7e9278..f9b3e42f0a0 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -103,6 +103,7 @@ import { MySQLIcon, Neo4jIcon, NotionIcon, + ObsidianIcon, OnePasswordIcon, OpenAIIcon, OutlookIcon, @@ -265,6 +266,7 @@ export const blockTypeToIconMap: Record = { mysql: MySQLIcon, neo4j: Neo4jIcon, notion_v2: NotionIcon, + obsidian: ObsidianIcon, onedrive: MicrosoftOneDriveIcon, onepassword: OnePasswordIcon, openai: OpenAIIcon, diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index f8d851049fe..a5de340aaa7 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -98,6 +98,7 @@ "mysql", "neo4j", "notion", + "obsidian", "onedrive", "onepassword", "openai", diff --git a/apps/docs/content/docs/en/tools/obsidian.mdx b/apps/docs/content/docs/en/tools/obsidian.mdx new file mode 100644 index 00000000000..c2b28f74cbf --- /dev/null +++ b/apps/docs/content/docs/en/tools/obsidian.mdx @@ -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" + + + +## 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 | + + diff --git a/apps/sim/blocks/blocks/obsidian.ts b/apps/sim/blocks/blocks/obsidian.ts new file mode 100644 index 00000000000..533c80fc654 --- /dev/null +++ b/apps/sim/blocks/blocks/obsidian.ts @@ -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 = {} + 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' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 7ff0b918dd1..23c47186ca4 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -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 = { note: NoteBlock, notion: NotionBlock, notion_v2: NotionV2Block, + obsidian: ObsidianBlock, onepassword: OnePasswordBlock, onedrive: OneDriveBlock, openai: OpenAIBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 5525e048cfa..41fa14fa22a 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -710,6 +710,155 @@ export function PerplexityIcon(props: SVGProps) { ) } +export function ObsidianIcon(props: SVGProps) { + 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 ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + export function NotionIcon(props: SVGProps) { return ( diff --git a/apps/sim/tools/obsidian/append_active.ts b/apps/sim/tools/obsidian/append_active.ts new file mode 100644 index 00000000000..49ec7378d75 --- /dev/null +++ b/apps/sim/tools/obsidian/append_active.ts @@ -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', + }, + }, +} diff --git a/apps/sim/tools/obsidian/append_note.ts b/apps/sim/tools/obsidian/append_note.ts new file mode 100644 index 00000000000..2f0fbed8094 --- /dev/null +++ b/apps/sim/tools/obsidian/append_note.ts @@ -0,0 +1,74 @@ +import type { ToolConfig } from '@/tools/types' +import type { ObsidianAppendNoteParams, ObsidianAppendNoteResponse } from './types' + +export const appendNoteTool: ToolConfig = { + 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', + }, + }, +} diff --git a/apps/sim/tools/obsidian/append_periodic_note.ts b/apps/sim/tools/obsidian/append_periodic_note.ts new file mode 100644 index 00000000000..50b43ea18cf --- /dev/null +++ b/apps/sim/tools/obsidian/append_periodic_note.ts @@ -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', + }, + }, +} diff --git a/apps/sim/tools/obsidian/create_note.ts b/apps/sim/tools/obsidian/create_note.ts new file mode 100644 index 00000000000..fed38cca8f6 --- /dev/null +++ b/apps/sim/tools/obsidian/create_note.ts @@ -0,0 +1,74 @@ +import type { ToolConfig } from '@/tools/types' +import type { ObsidianCreateNoteParams, ObsidianCreateNoteResponse } from './types' + +export const createNoteTool: ToolConfig = { + 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', + }, + }, +} diff --git a/apps/sim/tools/obsidian/delete_note.ts b/apps/sim/tools/obsidian/delete_note.ts new file mode 100644 index 00000000000..a6911d85e7f --- /dev/null +++ b/apps/sim/tools/obsidian/delete_note.ts @@ -0,0 +1,66 @@ +import type { ToolConfig } from '@/tools/types' +import type { ObsidianDeleteNoteParams, ObsidianDeleteNoteResponse } from './types' + +export const deleteNoteTool: ToolConfig = { + 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', + }, + }, +} diff --git a/apps/sim/tools/obsidian/execute_command.ts b/apps/sim/tools/obsidian/execute_command.ts new file mode 100644 index 00000000000..240711b6300 --- /dev/null +++ b/apps/sim/tools/obsidian/execute_command.ts @@ -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', + }, + }, +} diff --git a/apps/sim/tools/obsidian/get_active.ts b/apps/sim/tools/obsidian/get_active.ts new file mode 100644 index 00000000000..56a838d6716 --- /dev/null +++ b/apps/sim/tools/obsidian/get_active.ts @@ -0,0 +1,59 @@ +import type { ToolConfig } from '@/tools/types' +import type { ObsidianGetActiveParams, ObsidianGetActiveResponse } from './types' + +export const getActiveTool: ToolConfig = { + 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, + }, + }, +} diff --git a/apps/sim/tools/obsidian/get_note.ts b/apps/sim/tools/obsidian/get_note.ts new file mode 100644 index 00000000000..118cb7fa6c9 --- /dev/null +++ b/apps/sim/tools/obsidian/get_note.ts @@ -0,0 +1,68 @@ +import type { ToolConfig } from '@/tools/types' +import type { ObsidianGetNoteParams, ObsidianGetNoteResponse } from './types' + +export const getNoteTool: ToolConfig = { + 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', + }, + }, +} diff --git a/apps/sim/tools/obsidian/get_periodic_note.ts b/apps/sim/tools/obsidian/get_periodic_note.ts new file mode 100644 index 00000000000..d37b3169ac4 --- /dev/null +++ b/apps/sim/tools/obsidian/get_periodic_note.ts @@ -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', + }, + }, +} diff --git a/apps/sim/tools/obsidian/index.ts b/apps/sim/tools/obsidian/index.ts new file mode 100644 index 00000000000..43327f8971d --- /dev/null +++ b/apps/sim/tools/obsidian/index.ts @@ -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' diff --git a/apps/sim/tools/obsidian/list_commands.ts b/apps/sim/tools/obsidian/list_commands.ts new file mode 100644 index 00000000000..71394db09d0 --- /dev/null +++ b/apps/sim/tools/obsidian/list_commands.ts @@ -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' }, + }, + }, + }, +} diff --git a/apps/sim/tools/obsidian/list_files.ts b/apps/sim/tools/obsidian/list_files.ts new file mode 100644 index 00000000000..6c83880c14a --- /dev/null +++ b/apps/sim/tools/obsidian/list_files.ts @@ -0,0 +1,76 @@ +import type { ToolConfig } from '@/tools/types' +import type { ObsidianListFilesParams, ObsidianListFilesResponse } from './types' + +export const listFilesTool: ToolConfig = { + 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' }, + }, + }, + }, +} diff --git a/apps/sim/tools/obsidian/open_file.ts b/apps/sim/tools/obsidian/open_file.ts new file mode 100644 index 00000000000..4100ce1025e --- /dev/null +++ b/apps/sim/tools/obsidian/open_file.ts @@ -0,0 +1,73 @@ +import type { ToolConfig } from '@/tools/types' +import type { ObsidianOpenFileParams, ObsidianOpenFileResponse } from './types' + +export const openFileTool: ToolConfig = { + 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', + }, + }, +} diff --git a/apps/sim/tools/obsidian/patch_active.ts b/apps/sim/tools/obsidian/patch_active.ts new file mode 100644 index 00000000000..ae72a71218b --- /dev/null +++ b/apps/sim/tools/obsidian/patch_active.ts @@ -0,0 +1,107 @@ +import type { ToolConfig } from '@/tools/types' +import type { ObsidianPatchActiveParams, ObsidianPatchActiveResponse } from './types' + +export const patchActiveTool: ToolConfig = { + 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 = { + 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', + }, + }, +} diff --git a/apps/sim/tools/obsidian/patch_note.ts b/apps/sim/tools/obsidian/patch_note.ts new file mode 100644 index 00000000000..12013d8b77f --- /dev/null +++ b/apps/sim/tools/obsidian/patch_note.ts @@ -0,0 +1,118 @@ +import type { ToolConfig } from '@/tools/types' +import type { ObsidianPatchNoteParams, ObsidianPatchNoteResponse } from './types' + +export const patchNoteTool: ToolConfig = { + 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 = { + 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', + }, + }, +} diff --git a/apps/sim/tools/obsidian/search.ts b/apps/sim/tools/obsidian/search.ts new file mode 100644 index 00000000000..72551697f6c --- /dev/null +++ b/apps/sim/tools/obsidian/search.ts @@ -0,0 +1,95 @@ +import type { ToolConfig } from '@/tools/types' +import type { ObsidianSearchParams, ObsidianSearchResponse } from './types' + +export const searchTool: ToolConfig = { + 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' }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/obsidian/types.ts b/apps/sim/tools/obsidian/types.ts new file mode 100644 index 00000000000..6fe9203414a --- /dev/null +++ b/apps/sim/tools/obsidian/types.ts @@ -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 + } +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 3539724f68d..437c36a3777 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -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 = { 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,