Skip to main content
Glama
jrnlExecutor.ts5 kB
/** * Utility for executing jrnl CLI commands and parsing results */ import { spawn } from "child_process"; /** * Options for executing a jrnl command */ interface JrnlExecutorOptions { timeout?: number; journal?: string; maxBuffer?: number; } /** * Result of a jrnl command execution */ interface JrnlExecutionResult { stdout: string; stderr: string; success: boolean; } /** * Default options for jrnl execution */ const DEFAULT_OPTIONS: JrnlExecutorOptions = { timeout: 30000, // 30 seconds maxBuffer: 1024 * 1024 * 5, // 5MB }; /** * Execute a jrnl command and return the result * * @param args Command line arguments to pass to jrnl * @param options Execution options * @returns Promise with execution result */ export async function executeJrnlCommand( args: string[], options: JrnlExecutorOptions = {}, ): Promise<JrnlExecutionResult> { const opts = { ...DEFAULT_OPTIONS, ...options }; let commandArgs = [...args]; // If journal is specified, add the --journal flag if (opts.journal) { commandArgs = ["--journal", opts.journal, ...commandArgs]; } return new Promise((resolve) => { let stdout = ""; let stderr = ""; let killed = false; // Spawn jrnl process with arguments const process = spawn("jrnl", commandArgs); // Set timeout if specified const timeoutId = opts.timeout ? setTimeout(() => { process.kill(); killed = true; resolve({ stdout, stderr: stderr + "\nCommand timed out after " + opts.timeout + "ms", success: false, }); }, opts.timeout) : null; // Collect stdout process.stdout.on("data", (data) => { stdout += data.toString(); }); // Collect stderr process.stderr.on("data", (data) => { stderr += data.toString(); }); // Handle process completion process.on("close", (code) => { if (timeoutId) clearTimeout(timeoutId); if (!killed) { resolve({ stdout, stderr, success: code === 0, }); } }); // Handle errors process.on("error", (err) => { if (timeoutId) clearTimeout(timeoutId); if (!killed) { resolve({ stdout, stderr: stderr + "\n" + err.message, success: false, }); } }); }); } /** * Parse JSON output from jrnl command * * @param output Command output string * @returns Parsed JSON object */ export function parseJrnlJsonOutput<T>(output: string): T { try { return JSON.parse(output.trim()) as T; } catch (error) { if (error instanceof Error) { throw new Error(`Failed to parse jrnl output as JSON: ${error.message}`); } throw error; } } /** * Safely execute a jrnl command that outputs JSON and parse the result * * @param args Command line arguments * @param options Execution options * @returns Parsed JSON result */ export async function executeJrnlJsonCommand<T>( args: string[], options: JrnlExecutorOptions = {}, ): Promise<T> { // Ensure export flag is included to get JSON output const commandArgs = [...args]; if (!commandArgs.includes("--export") && !commandArgs.includes("-j")) { commandArgs.push("--export", "json"); } const result = await executeJrnlCommand(commandArgs, options); if (!result.success) { throw new Error(`jrnl command failed: ${result.stderr}`); } return parseJrnlJsonOutput<T>(result.stdout); } export class JrnlExecutor { async execute(args: string[]): Promise<string> { const result = await executeJrnlCommand(args); if (!result.success) { throw new Error(`jrnl command failed: ${result.stderr}`); } // Clean jrnl output - remove decorative boxes and extra text const cleaned = this.cleanJrnlOutput(result.stdout); return cleaned; } private cleanJrnlOutput(output: string): string { // Remove decorative box characters and summary lines const lines = output.split("\n"); const cleanedLines = lines.filter((line) => { // Filter out decorative box lines and summary text return ( !line.match(/^[┏┓┗┛━ ]+$/) && !line.match(/^\s*\d+ entries? found\s*$/) && !line.match(/^\s*no entries? found\s*$/) && !line.match(/^Journals defined in config/) && line.trim() !== "" ); }); const cleanedOutput = cleanedLines.join("\n").trim(); // Check if jrnl returned "no entries found" - return empty JSON structure if (output.includes("no entries found") || cleanedOutput === "") { return '{"tags": {}, "entries": []}'; } // For JSON export, try to extract just the JSON part if (cleanedOutput.includes("{") && cleanedOutput.includes("}")) { const jsonStart = cleanedOutput.indexOf("{"); const jsonEnd = cleanedOutput.lastIndexOf("}") + 1; return cleanedOutput.substring(jsonStart, jsonEnd); } return cleanedOutput; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/yostos/jrnl-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server