feat: add Obsidian CLI API service
Build and Push Docker Container / build-and-push (push) Successful in 4m26s
Build and Push Docker Container / build-and-push (push) Successful in 4m26s
- Add Docker image for the official Obsidian desktop app and CLI. - Start Obsidian headlessly with Xvfb and DBus setup. - Expose a safe structured HTTP command API without shell execution. - Add JWT-based vault, path, and command authorization. - Support single-vault and multi-vault container mounts. - Add TypeScript SDK helpers for Obsidian CLI commands. - Add n8n community node package with Obsidian operations. - Add docs, compose config, tests, and production image workflow.
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
# n8n-nodes-obsidian-api
|
||||
n8n community-node package for the Obsidian API Docker service.
|
||||
|
||||
It talks to the HTTP API, which then runs the **official Obsidian CLI** against mounted vaults. The node sends structured `args` arrays only. It does not send shell commands.
|
||||
|
||||
## Nodes
|
||||
|
||||
### Obsidian API
|
||||
|
||||
Resources:
|
||||
- **Note**
|
||||
- Create
|
||||
- Read
|
||||
- Append
|
||||
- Prepend
|
||||
- Delete
|
||||
- Rename
|
||||
- List Files
|
||||
- **Daily Note**
|
||||
- Append
|
||||
- Prepend
|
||||
- Read
|
||||
- Path
|
||||
- **Search**
|
||||
- Search
|
||||
- Search With Context
|
||||
- **Vault**
|
||||
- Info
|
||||
- List Vaults
|
||||
- **Raw Command**
|
||||
- Safe args-array escape hatch for any official CLI command
|
||||
|
||||
## Credentials
|
||||
Credential type: **Obsidian API**
|
||||
|
||||
Fields:
|
||||
- **Base URL**: e.g. `http://obsidian-api:8080`
|
||||
- **API Token**: JWT created with `scripts/create_jwt.py`
|
||||
- **Default Vault**: optional, e.g. `work`
|
||||
|
||||
## Multi-vault
|
||||
Set the `Vault` field on a node to override the credential default vault.
|
||||
|
||||
The node sends this as the official CLI prefix:
|
||||
```text
|
||||
vault=work
|
||||
```
|
||||
|
||||
Raw command example:
|
||||
```json
|
||||
["vault=work", "read", "path=Inbox/Note.md"]
|
||||
```
|
||||
|
||||
## Local install in self-hosted n8n
|
||||
From this repo:
|
||||
```bash
|
||||
cd packages/n8n-nodes-obsidian-api
|
||||
npm install --ignore-scripts
|
||||
npm run build
|
||||
npm pack
|
||||
```
|
||||
|
||||
Then install the generated `.tgz` in your n8n custom extensions setup.
|
||||
|
||||
For Docker-based n8n, one common setup is mounting this package into n8n's custom extensions directory and setting:
|
||||
```env
|
||||
N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom
|
||||
```
|
||||
|
||||
Exact installation depends on your n8n deployment.
|
||||
|
||||
## Development
|
||||
```bash
|
||||
npm install --ignore-scripts
|
||||
npm run typecheck
|
||||
npm run build
|
||||
```
|
||||
|
||||
`--ignore-scripts` avoids local native builds from n8n transitive dependencies on unsupported bleeding-edge Node versions. Use the Node version recommended by your n8n release for production builds.
|
||||
@@ -0,0 +1,40 @@
|
||||
import type {
|
||||
ICredentialType,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export class ObsidianApi implements ICredentialType {
|
||||
name = 'obsidianApi';
|
||||
displayName = 'Obsidian API';
|
||||
documentationUrl = 'https://git.yiprawr.dev/Docker/obsidian-api';
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Base URL',
|
||||
name: 'baseUrl',
|
||||
type: 'string',
|
||||
default: 'http://obsidian-api:8080',
|
||||
placeholder: 'http://obsidian-api:8080',
|
||||
description: 'Base URL of the Obsidian API Docker service',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'API Token',
|
||||
name: 'apiToken',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
password: true,
|
||||
},
|
||||
default: '',
|
||||
description: 'Bearer JWT created with scripts/create_jwt.py',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Default Vault',
|
||||
name: 'defaultVault',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'work',
|
||||
description: 'Optional default vault name/path. Sent as vault=<value> before every command unless the node overrides it.',
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export {};
|
||||
@@ -0,0 +1,324 @@
|
||||
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: 'Vault',
|
||||
name: 'vault',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'work',
|
||||
description: 'Optional vault name/path. Overrides the credential default vault. Sent as vault=<value>.',
|
||||
},
|
||||
{
|
||||
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',
|
||||
},
|
||||
{
|
||||
displayName: 'Verbose',
|
||||
name: 'verbose',
|
||||
type: 'boolean',
|
||||
displayOptions: { show: { resource: ['vault'], operation: ['vaults'] } },
|
||||
default: true,
|
||||
},
|
||||
|
||||
// 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: 'Timeout Seconds',
|
||||
name: 'timeout',
|
||||
type: 'number',
|
||||
default: 60,
|
||||
description: 'Command timeout in seconds',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
const resource = this.getNodeParameter('resource', itemIndex) as string;
|
||||
const operation = this.getNodeParameter('operation', itemIndex) as string;
|
||||
const vault = this.getNodeParameter('vault', itemIndex, '') as string;
|
||||
const timeout = this.getNodeParameter('timeout', itemIndex, 60) as number;
|
||||
|
||||
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);
|
||||
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 });
|
||||
returnData.push({ json: result, pairedItem: { item: itemIndex } });
|
||||
}
|
||||
|
||||
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): string[] {
|
||||
switch (operation) {
|
||||
case 'vaultInfo':
|
||||
return ['vault'];
|
||||
case 'vaults':
|
||||
return compactArgs(['vaults', kv('verbose', context.getNodeParameter('verbose', itemIndex) as boolean)]);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<svg fill="none" height="100%" width="100%" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<radialGradient id="logo-bottom-left" cx="0" cy="0" gradientTransform="matrix(-59 -225 150 -39 161.4 470)" gradientUnits="userSpaceOnUse" r="1">
|
||||
<stop offset="0" stop-color="#fff" stop-opacity=".4"/>
|
||||
<stop offset="1" stop-opacity=".1"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="logo-top-right" cx="0" cy="0" gradientTransform="matrix(50 -379 280 37 360 374.2)" gradientUnits="userSpaceOnUse" r="1">
|
||||
<stop offset="0" stop-color="#fff" stop-opacity=".6"/>
|
||||
<stop offset="1" stop-color="#fff" stop-opacity=".1"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="logo-top-left" cx="0" cy="0" gradientTransform="matrix(69 -319 218 47 175.4 307)" gradientUnits="userSpaceOnUse" r="1">
|
||||
<stop offset="0" stop-color="#fff" stop-opacity=".8"/>
|
||||
<stop offset="1" stop-color="#fff" stop-opacity=".4"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="logo-bottom-right" cx="0" cy="0" gradientTransform="matrix(-96 -163 187 -111 335.3 512.2)" gradientUnits="userSpaceOnUse" r="1">
|
||||
<stop offset="0" stop-color="#fff" stop-opacity=".3"/>
|
||||
<stop offset="1" stop-opacity=".3"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="logo-top-edge" cx="0" cy="0" gradientTransform="matrix(-36 166 -112 -24 310 128.2)" gradientUnits="userSpaceOnUse" r="1">
|
||||
<stop offset="0" stop-color="#fff" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#fff" stop-opacity=".2"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="logo-left-edge" cx="0" cy="0" gradientTransform="matrix(88 89 -190 187 111 220.2)" gradientUnits="userSpaceOnUse" r="1">
|
||||
<stop offset="0" stop-color="#fff" stop-opacity=".2"/>
|
||||
<stop offset="1" stop-color="#fff" stop-opacity=".4"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="logo-bottom-edge" cx="0" cy="0" gradientTransform="matrix(9 130 -276 20 215 284)" gradientUnits="userSpaceOnUse" r="1">
|
||||
<stop offset="0" stop-color="#fff" stop-opacity=".2"/>
|
||||
<stop offset="1" stop-color="#fff" stop-opacity=".3"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="logo-middle-edge" cx="0" cy="0" gradientTransform="matrix(-198 -104 327 -623 400 399.2)" gradientUnits="userSpaceOnUse" r="1">
|
||||
<stop offset="0" stop-color="#fff" stop-opacity=".2"/>
|
||||
<stop offset=".5" stop-color="#fff" stop-opacity=".2"/>
|
||||
<stop offset="1" stop-color="#fff" stop-opacity=".3"/>
|
||||
</radialGradient>
|
||||
<clipPath id="clip">
|
||||
<path d="M.2.2h512v512H.2z"/>
|
||||
</clipPath>
|
||||
<g clip-path="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(#logo-bottom-left)"/>
|
||||
<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(#logo-top-right)"/>
|
||||
<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(#logo-top-left)"/>
|
||||
<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(#logo-bottom-right)"/>
|
||||
<g clip-rule="evenodd" fill-rule="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(#logo-top-edge)"/>
|
||||
<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(#logo-left-edge)"/>
|
||||
<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(#logo-bottom-edge)"/>
|
||||
<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(#logo-middle-edge)"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
+2304
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "n8n-nodes-obsidian-api",
|
||||
"version": "0.1.0",
|
||||
"description": "n8n community nodes for the Obsidian API Docker service backed by the official Obsidian CLI.",
|
||||
"license": "MIT",
|
||||
"homepage": "https://git.yiprawr.dev/Docker/obsidian-api",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+ssh://git@git.yiprawr.dev/Docker/obsidian-api.git",
|
||||
"directory": "packages/n8n-nodes-obsidian-api"
|
||||
},
|
||||
"keywords": [
|
||||
"n8n-community-node-package",
|
||||
"n8n",
|
||||
"obsidian",
|
||||
"obsidian-cli",
|
||||
"notes"
|
||||
],
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json && mkdir -p dist/nodes/ObsidianApi && cp nodes/ObsidianApi/obsidian.svg dist/nodes/ObsidianApi/obsidian.svg",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"lint": "eslint nodes credentials shared --ext .ts"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"nodes",
|
||||
"credentials",
|
||||
"shared"
|
||||
],
|
||||
"n8n": {
|
||||
"n8nNodesApiVersion": 1,
|
||||
"credentials": [
|
||||
"dist/credentials/ObsidianApi.credentials.js"
|
||||
],
|
||||
"nodes": [
|
||||
"dist/nodes/ObsidianApi/ObsidianApi.node.js"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.0.0",
|
||||
"eslint": "^9.29.0",
|
||||
"n8n-workflow": "^1.82.0",
|
||||
"typescript": "^5.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"n8n-workflow": "*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
IDataObject,
|
||||
IHttpRequestOptions,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
export interface ObsidianCommandResult extends IDataObject {
|
||||
id: string;
|
||||
command: string[];
|
||||
cwd: string;
|
||||
exit_code: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
duration_ms: number;
|
||||
timed_out: boolean;
|
||||
}
|
||||
|
||||
export interface ObsidianCredentials extends IDataObject {
|
||||
baseUrl: string;
|
||||
apiToken: string;
|
||||
defaultVault?: string;
|
||||
}
|
||||
|
||||
export function compactArgs(args: Array<string | undefined | null | false>): string[] {
|
||||
return args.filter((arg): arg is string => typeof arg === 'string' && arg.length > 0);
|
||||
}
|
||||
|
||||
export function kv(name: string, value: string | number | boolean | undefined | null): string | undefined {
|
||||
if (value === undefined || value === null || value === '') return undefined;
|
||||
if (typeof value === 'boolean') return value ? name : undefined;
|
||||
return `${name}=${value}`;
|
||||
}
|
||||
|
||||
export function withVault(args: string[], vault?: string): string[] {
|
||||
if (!vault || args[0]?.startsWith('vault=')) return args;
|
||||
return [`vault=${vault}`, ...args];
|
||||
}
|
||||
|
||||
export async function executeObsidianCommand(
|
||||
context: IExecuteFunctions,
|
||||
itemIndex: number,
|
||||
args: string[],
|
||||
options: { vault?: string; timeout?: number; stdin?: string } = {},
|
||||
): Promise<ObsidianCommandResult> {
|
||||
const credentials = await context.getCredentials('obsidianApi') as ObsidianCredentials;
|
||||
const baseUrl = String(credentials.baseUrl).replace(/\/$/, '');
|
||||
const vault = options.vault || String(credentials.defaultVault || '');
|
||||
const finalArgs = withVault(args, vault);
|
||||
|
||||
const request: IHttpRequestOptions = {
|
||||
method: 'POST',
|
||||
url: `${baseUrl}/commands`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${credentials.apiToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: {
|
||||
args: finalArgs,
|
||||
stdin: options.stdin || undefined,
|
||||
timeout: options.timeout,
|
||||
},
|
||||
json: true,
|
||||
};
|
||||
|
||||
const result = await context.helpers.httpRequest(request) as ObsidianCommandResult;
|
||||
|
||||
if (result.exit_code !== 0) {
|
||||
throw new NodeOperationError(
|
||||
context.getNode(),
|
||||
`Obsidian CLI command failed with exit code ${result.exit_code}: ${result.stderr || result.stdout}`,
|
||||
{ itemIndex },
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "Node",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": ".",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["credentials/**/*.ts", "nodes/**/*.ts", "shared/**/*.ts", "index.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user