import { Injectable } from '@nestjs/common';
import { Tool, type Context } from '@rekog/mcp-nest';
import { z } from 'zod';
import { LogseqClient } from '../logseq/logseq.client';
import { LogseqFormatter } from '../logseq/logseq.formatter';
/**
* Journal Tool - 일일 기록 관리
*
* User Flow: Daily Recording
* - 오늘의 Journal에 기록 추가
* - 특정 날짜의 Journal 조회
*
* OCP: @Tool 데코레이터로 새로운 도구 추가 시 기존 코드 수정 불필요
*/
@Injectable()
export class JournalTool {
constructor(
private readonly logseq: LogseqClient,
private readonly formatter: LogseqFormatter,
) {}
@Tool({
name: 'journal-add-entry',
description:
'오늘의 Journal(일지)에 새로운 내용을 기록합니다. 미팅 내용, 할 일, 생각 등을 기록할 때 사용하세요.',
parameters: z.object({
content: z.string().describe('기록할 내용. Markdown 형식을 지원합니다.'),
asTask: z
.boolean()
.optional()
.describe('TODO 마커를 추가하여 할 일로 기록할지 여부'),
}),
annotations: {
title: 'Journal에 기록 추가',
readOnlyHint: false,
idempotentHint: false,
},
})
async addEntry(
{ content, asTask }: { content: string; asTask?: boolean },
context: Context,
) {
await context.reportProgress({ progress: 10, total: 100 });
const journalName = this.logseq.getTodayJournalName();
await context.reportProgress({ progress: 50, total: 100 });
// 안전한 파싱 사용 - Logseq 호환성 검증 포함
// (하나의 블록에 여러 리스트/헤딩이 있으면 자동 분리)
const parsedBlocks = this.formatter.safeParseToBlocks(content);
// asTask가 true이면 첫 번째 블록에 TODO 마커 추가
if (asTask && parsedBlocks.length > 0) {
parsedBlocks[0].content = `TODO ${parsedBlocks[0].content}`;
}
const block = await this.logseq.appendBlocksInPage(
journalName,
parsedBlocks,
);
await context.reportProgress({ progress: 100, total: 100 });
if (!block) {
return {
content: [
{
type: 'text',
text: `❌ Journal에 기록 실패. Logseq이 실행 중인지 확인해주세요.`,
},
],
};
}
return {
content: [
{
type: 'text',
text: `✅ ${journalName}에 기록했습니다:\n${asTask ? '☐ ' : ''}${content}`,
},
],
};
}
@Tool({
name: 'journal-get-today',
description:
'오늘의 Journal 내용을 조회합니다. 오늘 기록한 내용, 할 일 등을 확인할 수 있습니다.',
parameters: z.object({}),
annotations: {
title: '오늘의 Journal 조회',
readOnlyHint: true,
idempotentHint: true,
},
})
async getToday(_args: object, context: Context) {
await context.reportProgress({ progress: 20, total: 100 });
const journalName = this.logseq.getTodayJournalName();
const blocks = await this.logseq.getPageBlocksTree(journalName);
await context.reportProgress({ progress: 100, total: 100 });
if (!blocks || blocks.length === 0) {
return {
content: [
{
type: 'text',
text: `📅 ${journalName}\n\n아직 기록이 없습니다.`,
},
],
};
}
const contents = this.formatBlocksTree(blocks);
return {
content: [
{
type: 'text',
text: `📅 ${journalName}\n\n${contents}`,
},
],
};
}
@Tool({
name: 'journal-get-date',
description: '특정 날짜의 Journal 내용을 조회합니다.',
parameters: z.object({
date: z
.string()
.describe('조회할 날짜 (YYYY-MM-DD 형식, 예: 2024-12-01)'),
}),
annotations: {
title: '특정 날짜 Journal 조회',
readOnlyHint: true,
idempotentHint: true,
},
})
async getByDate({ date }: { date: string }, context: Context) {
await context.reportProgress({ progress: 20, total: 100 });
const dateObj = new Date(date);
if (isNaN(dateObj.getTime())) {
return {
content: [
{
type: 'text',
text: `❌ 잘못된 날짜 형식입니다. YYYY-MM-DD 형식으로 입력해주세요.`,
},
],
};
}
const journalName = this.logseq.getJournalNameForDate(dateObj);
const blocks = await this.logseq.getPageBlocksTree(journalName);
await context.reportProgress({ progress: 100, total: 100 });
if (!blocks || blocks.length === 0) {
return {
content: [
{
type: 'text',
text: `📅 ${journalName}\n\n기록이 없습니다.`,
},
],
};
}
const contents = this.formatBlocksTree(blocks);
return {
content: [
{
type: 'text',
text: `📅 ${journalName}\n\n${contents}`,
},
],
};
}
/**
* Block 트리를 텍스트로 포맷팅
*/
private formatBlocksTree(
blocks: Array<{
content: string;
children?: Array<{ content: string; children?: unknown[] }>;
}>,
indent = 0,
): string {
return blocks
.map((block) => {
const prefix = ' '.repeat(indent) + (indent > 0 ? '• ' : '');
let text = prefix + block.content;
if (block.children && block.children.length > 0) {
text +=
'\n' +
this.formatBlocksTree(
block.children as Array<{
content: string;
children?: Array<{ content: string; children?: unknown[] }>;
}>,
indent + 1,
);
}
return text;
})
.join('\n');
}
}