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,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;
|
||||
}
|
||||
Reference in New Issue
Block a user