import type { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription, } from 'n8n-workflow'; import { NodeOperationError } from 'n8n-workflow'; import { compactArgs, executeObsidianCommand, kv } from '../../shared/ObsidianApiClient'; export class ObsidianApi implements INodeType { description: INodeTypeDescription = { displayName: 'Obsidian API', name: 'obsidianApi', icon: 'file:obsidian.svg', group: ['transform'], version: 1, subtitle: '={{$parameter["operation"]}}', description: 'Run official Obsidian CLI commands through the Obsidian API Docker service', defaults: { name: 'Obsidian API', }, inputs: ['main'], outputs: ['main'], credentials: [ { name: 'obsidianApi', required: true, }, ], properties: [ { displayName: 'Resource', name: 'resource', type: 'options', noDataExpression: true, options: [ { name: 'Daily Note', value: 'daily' }, { name: 'Note', value: 'note' }, { name: 'Raw Command', value: 'raw' }, { name: 'Search', value: 'search' }, { name: 'Vault', value: 'vault' }, ], default: 'note', }, // Note operations { displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true, displayOptions: { show: { resource: ['note'] } }, options: [ { name: 'Append', value: 'append' }, { name: 'Create', value: 'create' }, { name: 'Delete', value: 'delete' }, { name: 'List Files', value: 'files' }, { name: 'Prepend', value: 'prepend' }, { name: 'Read', value: 'read' }, { name: 'Rename', value: 'rename' }, ], default: 'create', }, { displayName: 'Path', name: 'path', type: 'string', displayOptions: { show: { resource: ['note'], operation: ['append', 'create', 'delete', 'prepend', 'read', 'rename'] } }, default: '', placeholder: 'Inbox/Note.md', required: true, }, { displayName: 'Content', name: 'content', type: 'string', typeOptions: { rows: 8 }, displayOptions: { show: { resource: ['note'], operation: ['append', 'create', 'prepend'] } }, default: '', required: true, }, { displayName: 'Overwrite', name: 'overwrite', type: 'boolean', displayOptions: { show: { resource: ['note'], operation: ['create'] } }, default: false, }, { displayName: 'New Name', name: 'newName', type: 'string', displayOptions: { show: { resource: ['note'], operation: ['rename'] } }, default: '', required: true, }, { displayName: 'Folder', name: 'folder', type: 'string', displayOptions: { show: { resource: ['note'], operation: ['files'] } }, default: '', placeholder: 'Inbox', }, // Daily note operations { displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true, displayOptions: { show: { resource: ['daily'] } }, options: [ { name: 'Append', value: 'dailyAppend' }, { name: 'Path', value: 'dailyPath' }, { name: 'Prepend', value: 'dailyPrepend' }, { name: 'Read', value: 'dailyRead' }, ], default: 'dailyAppend', }, { displayName: 'Content', name: 'dailyContent', type: 'string', typeOptions: { rows: 8 }, displayOptions: { show: { resource: ['daily'], operation: ['dailyAppend', 'dailyPrepend'] } }, default: '', required: true, }, // Search operations { displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true, displayOptions: { show: { resource: ['search'] } }, options: [ { name: 'Search', value: 'search' }, { name: 'Search With Context', value: 'searchContext' }, ], default: 'search', }, { displayName: 'Query', name: 'query', type: 'string', displayOptions: { show: { resource: ['search'] } }, default: '', required: true, }, { displayName: 'Format', name: 'format', type: 'options', displayOptions: { show: { resource: ['search'] } }, options: [ { name: 'Text', value: 'text' }, { name: 'JSON', value: 'json' }, ], default: 'text', }, // Vault operations { displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true, displayOptions: { show: { resource: ['vault'] } }, options: [ { name: 'Info', value: 'vaultInfo' }, { name: 'List Vaults', value: 'vaults' }, ], default: 'vaultInfo', }, // Raw command { displayName: 'Args JSON', name: 'rawArgs', type: 'json', displayOptions: { show: { resource: ['raw'] } }, default: '["version"]', description: 'Structured CLI args array. Example: ["create", "path=Inbox/Test.md", "content=Hello", "overwrite"]. Do not include shell strings.', required: true, }, { displayName: 'Additional Fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', default: {}, options: [ { displayName: 'Timeout Seconds', name: 'timeout', type: 'number', default: 60, description: 'Command timeout in seconds', }, { displayName: 'Vault', name: 'vault', type: 'string', default: '', placeholder: 'work', description: 'Optional vault name/path. Overrides the credential default vault. Sent as vault=.', }, { displayName: 'Verbose', name: 'verbose', type: 'boolean', default: true, description: 'Only used by the List Vaults operation', }, ], }, ], }; async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); const returnData: INodeExecutionData[] = []; for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { try { const resource = this.getNodeParameter('resource', itemIndex) as string; const operation = this.getNodeParameter('operation', itemIndex) as string; const additionalFields = this.getNodeParameter('additionalFields', itemIndex, {}) as Record; const vault = String(additionalFields.vault || ''); const timeout = Number(additionalFields.timeout ?? 60); let args: string[]; switch (resource) { case 'note': { args = buildNoteArgs(this, operation, itemIndex); break; } case 'daily': { args = buildDailyArgs(this, operation, itemIndex); break; } case 'search': { args = buildSearchArgs(this, operation, itemIndex); break; } case 'vault': { args = buildVaultArgs(this, operation, itemIndex, additionalFields); break; } case 'raw': { args = parseRawArgs(this, itemIndex); break; } default: throw new NodeOperationError(this.getNode(), `Unsupported resource: ${resource}`, { itemIndex }); } const result = await executeObsidianCommand(this, itemIndex, args, { vault, timeout }); // Always keep n8n output minimal/clean. // - Prefer parsed stdout (e.g. vault/vaults) // - Otherwise keep stdout as-is // Build a stable, minimal output shape for n8n. // We try to expose the single most useful field name per operation. const stdout = result.stdout ?? ''; // Daily notes if (resource === 'daily' && operation === 'dailyPath') { const out: any = { path: stdout }; if (result.stderr) out.stderr = result.stderr; if (result.timed_out) out.timed_out = result.timed_out; returnData.push({ json: out, pairedItem: { item: itemIndex } }); continue; } if (resource === 'daily' && operation === 'dailyRead') { const out: any = { content: stdout }; if (result.stderr) out.stderr = result.stderr; if (result.timed_out) out.timed_out = result.timed_out; returnData.push({ json: out, pairedItem: { item: itemIndex } }); continue; } // Notes if (resource === 'note' && operation === 'read') { const out: any = { content: stdout }; if (result.stderr) out.stderr = result.stderr; if (result.timed_out) out.timed_out = result.timed_out; returnData.push({ json: out, pairedItem: { item: itemIndex } }); continue; } // vault and vaults are parsed as structured JSON const parsed = (result as any).parsed_stdout; if (parsed) { const out: any = parsed; if (result.timed_out) out.timed_out = result.timed_out; if (result.stderr) out.stderr = result.stderr; returnData.push({ json: out, pairedItem: { item: itemIndex } }); continue; } // Everything else (writes, deletes, rename, list files, etc.): // keep output clean/stable and only confirm success. const out: any = { ok: true }; if (result.stderr) out.stderr = result.stderr; if (result.timed_out) out.timed_out = result.timed_out; returnData.push({ json: out, pairedItem: { item: itemIndex } }); } catch (error) { if (this.continueOnFail()) { const message = error instanceof Error ? error.message : String(error); returnData.push({ json: { error: message }, pairedItem: { item: itemIndex } }); continue; } throw error; } } return [returnData]; } } function buildNoteArgs(context: IExecuteFunctions, operation: string, itemIndex: number): string[] { const path = context.getNodeParameter('path', itemIndex, '') as string; switch (operation) { case 'create': return compactArgs(['create', kv('path', path), kv('content', context.getNodeParameter('content', itemIndex) as string), kv('overwrite', context.getNodeParameter('overwrite', itemIndex) as boolean)]); case 'read': return compactArgs(['read', kv('path', path)]); case 'append': return compactArgs(['append', kv('path', path), kv('content', context.getNodeParameter('content', itemIndex) as string)]); case 'prepend': return compactArgs(['prepend', kv('path', path), kv('content', context.getNodeParameter('content', itemIndex) as string)]); case 'delete': return compactArgs(['delete', kv('path', path)]); case 'rename': return compactArgs(['rename', kv('path', path), kv('name', context.getNodeParameter('newName', itemIndex) as string)]); case 'files': return compactArgs(['files', kv('folder', context.getNodeParameter('folder', itemIndex, '') as string)]); default: throw new NodeOperationError(context.getNode(), `Unsupported note operation: ${operation}`, { itemIndex }); } } function buildDailyArgs(context: IExecuteFunctions, operation: string, itemIndex: number): string[] { switch (operation) { case 'dailyAppend': return compactArgs(['daily:append', kv('content', context.getNodeParameter('dailyContent', itemIndex) as string)]); case 'dailyPrepend': return compactArgs(['daily:prepend', kv('content', context.getNodeParameter('dailyContent', itemIndex) as string)]); case 'dailyRead': return ['daily:read']; case 'dailyPath': return ['daily:path']; default: throw new NodeOperationError(context.getNode(), `Unsupported daily operation: ${operation}`, { itemIndex }); } } function buildSearchArgs(context: IExecuteFunctions, operation: string, itemIndex: number): string[] { const command = operation === 'searchContext' ? 'search:context' : 'search'; return compactArgs([ command, kv('query', context.getNodeParameter('query', itemIndex) as string), kv('format', context.getNodeParameter('format', itemIndex) as string), ]); } function buildVaultArgs(context: IExecuteFunctions, operation: string, itemIndex: number, additionalFields: Record): string[] { switch (operation) { case 'vaultInfo': return ['vault']; case 'vaults': { const verbose = additionalFields.verbose === undefined ? true : Boolean(additionalFields.verbose); return compactArgs(['vaults', kv('verbose', verbose)]); } default: throw new NodeOperationError(context.getNode(), `Unsupported vault operation: ${operation}`, { itemIndex }); } } function parseRawArgs(context: IExecuteFunctions, itemIndex: number): string[] { const raw = context.getNodeParameter('rawArgs', itemIndex) as string | string[]; const parsed = Array.isArray(raw) ? raw : JSON.parse(raw); if (!Array.isArray(parsed) || !parsed.every((item) => typeof item === 'string')) { throw new NodeOperationError(context.getNode(), 'Args JSON must be an array of strings', { itemIndex }); } return parsed; }