← 목록으로
2026-02-26plans

RAG Knowledge Base v1.0 구현 플랜

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: 컨텍스트 소실 문제 근본 해결 — 보고서/CEO 지시/계획 파일을 자동 RAG 저장하고, 표준 검색 프로토콜 및 Compact 백업 체계를 구축한다.

Architecture: 신규 knowledge_base 테이블(NeonDB jarvis-rag)을 생성하여 범용 문서를 저장한다. ingest-document.ts가 단건 저장 핵심, ingest-directory.ts가 배치 저장을 담당한다. vice-reply.sh에 RAG 훅을 추가해 보고서가 생성될 때마다 자동 저장된다.

Tech Stack: Node.js 22, TypeScript ESM, pg (PostgreSQL), OpenAI text-embedding-3-small, NeonDB pgvector


전제 조건

  • RAG_DB_URL 환경변수에 jarvis-rag NeonDB URL이 설정되어 있어야 함
  • .env 파일 경로: projects/rag-knowledge-base/.env
  • CLAUDE.md에 명시된 값:
    RAG_DB_URL=postgresql://neondb_owner:npg_JcHZf5IQs2vq@ep-empty-smoke-a13aust8.ap-southeast-1.aws.neon.tech/neondb?sslmode=require
    
  • 작업 디렉토리: /Users/nbs22/(Claude)/(claude).projects/business-builder/projects/rag-knowledge-base

Task 1: .env + config.ts — RAG_DB_URL 지원

목적: 현재 config.ts는 DB_URL을 사용하지만 신규 jarvis-rag DB는 RAG_DB_URL에 있다. RAG_DB_URL 우선 사용으로 변경한다.

Files:

  • Modify: src/config.ts
  • Modify: .env (RAG_DB_URL 추가)
  • Modify: .env.example

Step 1: .env에 RAG_DB_URL 추가

# .env 파일에 아래 줄 추가 (CLAUDE.md의 값 사용)
echo 'RAG_DB_URL=postgresql://neondb_owner:npg_JcHZf5IQs2vq@ep-empty-smoke-a13aust8.ap-southeast-1.aws.neon.tech/neondb?sslmode=require' >> .env

Step 2: config.ts 수정

기존 6번째 줄:

const DB_URL = process.env.DB_URL ||
  'postgresql://neondb_owner:npg_OWVKrmC21gNk@ep-divine-darkness-a1gvyg6j-pooler.ap-southeast-1.aws.neon.tech/neondb?sslmode=require';

변경 후:

// RAG_DB_URL 우선, DB_URL fallback (하드코딩 fallback 제거)
const DB_URL = process.env.RAG_DB_URL || process.env.DB_URL;
if (!DB_URL) {
  throw new Error('RAG_DB_URL 또는 DB_URL 환경변수가 필요합니다.');
}

Step 3: .env.example 업데이트

OPENAI_API_KEY=sk-your-openai-api-key-here
RAG_DB_URL=postgresql://neondb_owner:YOUR_PASSWORD@YOUR_HOST.neon.tech/neondb?sslmode=require
DB_URL=postgresql://neondb_owner:YOUR_PASSWORD@YOUR_HOST.neon.tech/neondb?sslmode=require

Step 4: 빌드 및 동작 확인

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder/projects/rag-knowledge-base
npm run build

Expected: 빌드 오류 없음

node dist/search.js "의사결정" 3

Expected: 기존 task_comments 검색 결과 3건 출력

Step 5: Commit

git add src/config.ts .env.example
git commit -m "feat: config.ts RAG_DB_URL 우선 사용으로 변경"

Task 2: DB 스키마 — knowledge_base 테이블 생성

목적: 보고서/계획/Obsidian 문서를 저장할 범용 테이블을 jarvis-rag DB에 생성한다.

Files:

  • Create: src/setup-knowledge-base.ts

Step 1: setup-knowledge-base.ts 생성

// src/setup-knowledge-base.ts
import { getPool, EMBEDDING_DIMENSIONS } from './config.js';

/**
 * knowledge_base 테이블 생성 (최초 1회 실행).
 * task_comments 외에 보고서/계획/Obsidian 문서를 저장하는 범용 테이블.
 */
async function setupKnowledgeBase(): Promise<void> {
  const pool = getPool();

  try {
    console.log('=== knowledge_base 테이블 설정 시작 ===\n');

    // 1. pgvector 확인 (이미 활성화되어 있어야 함)
    await pool.query('CREATE EXTENSION IF NOT EXISTS vector;');
    console.log('1. pgvector 확장 확인 완료');

    // 2. knowledge_base 테이블 생성
    await pool.query(`
      CREATE TABLE IF NOT EXISTS knowledge_base (
        id           UUID PRIMARY KEY DEFAULT gen_random_uuid(),
        source_type  TEXT NOT NULL
                       CHECK (source_type IN ('report','instruction','plan','obsidian','compact_backup','decision','other')),
        source_path  TEXT,
        source_date  TIMESTAMPTZ,
        task_id      TEXT,
        title        TEXT NOT NULL,
        content      TEXT NOT NULL,
        content_hash TEXT,
        metadata     JSONB,
        embedding    vector(${EMBEDDING_DIMENSIONS}),
        created_at   TIMESTAMPTZ DEFAULT NOW()
      );
    `);
    console.log('2. knowledge_base 테이블 생성 완료');

    // 3. content_hash 유니크 인덱스 (중복 저장 방지)
    await pool.query(`
      CREATE UNIQUE INDEX IF NOT EXISTS idx_kb_content_hash
      ON knowledge_base (content_hash)
      WHERE content_hash IS NOT NULL;
    `);
    console.log('3. content_hash 유니크 인덱스 생성 완료');

    // 4. HNSW 벡터 인덱스
    const indexExists = await pool.query(`
      SELECT indexname FROM pg_indexes
      WHERE tablename = 'knowledge_base' AND indexname = 'idx_kb_embedding';
    `);

    if (indexExists.rows.length > 0) {
      console.log('4. 벡터 인덱스 이미 존재, 스킵');
    } else {
      await pool.query(`
        CREATE INDEX idx_kb_embedding
        ON knowledge_base
        USING hnsw (embedding vector_cosine_ops)
        WITH (m = 16, ef_construction = 64);
      `);
      console.log('4. HNSW 벡터 인덱스 생성 완료');
    }

    // 5. source_type 인덱스 (필터링용)
    await pool.query(`
      CREATE INDEX IF NOT EXISTS idx_kb_source_type
      ON knowledge_base (source_type);
    `);
    console.log('5. source_type 인덱스 생성 완료');

    console.log('\n=== knowledge_base 테이블 설정 완료 ===');

  } catch (error) {
    console.error('설정 중 오류:', error);
    throw error;
  } finally {
    await pool.end();
  }
}

setupKnowledgeBase().catch((err) => {
  console.error('Fatal error:', err);
  process.exit(1);
});

Step 2: package.json scripts 추가

"setup-kb": "node dist/setup-knowledge-base.js" 추가

Step 3: 빌드 및 실행

npm run build
node dist/setup-knowledge-base.js

Expected:

=== knowledge_base 테이블 설정 시작 ===
1. pgvector 확장 확인 완료
2. knowledge_base 테이블 생성 완료
3. content_hash 유니크 인덱스 생성 완료
4. HNSW 벡터 인덱스 생성 완료
5. source_type 인덱스 생성 완료
=== knowledge_base 테이블 설정 완료 ===

Step 4: Commit

git add src/setup-knowledge-base.ts package.json
git commit -m "feat: knowledge_base 테이블 스키마 추가"

Task 3: ingest-document.ts — 단건 문서 저장 핵심 모듈

목적: 텍스트 한 건을 knowledge_base에 INSERT + embedding 자동 생성. content_hash로 중복 저장 방지. 함수 export로 다른 모듈에서 import 가능.

Files:

  • Create: src/ingest-document.ts

Step 1: ingest-document.ts 생성

// src/ingest-document.ts
import crypto from 'crypto';
import { getPool } from './config.js';
import { embedText } from './embeddings.js';

export interface IngestParams {
  source_type: 'report' | 'instruction' | 'plan' | 'obsidian' | 'compact_backup' | 'decision' | 'other';
  title: string;
  content: string;
  source_path?: string;
  source_date?: Date | string;
  task_id?: string;
  metadata?: Record<string, unknown>;
}

export interface IngestResult {
  id: string;
  embedded: boolean;
  skipped: boolean;  // content_hash 중복으로 스킵된 경우
}

/**
 * 문서 1건을 knowledge_base에 저장하고 embedding을 생성한다.
 * content_hash가 동일한 문서는 중복 저장하지 않는다.
 * 다른 모듈에서 import하여 사용 가능.
 */
export async function ingestDocument(
  params: IngestParams,
  externalPool?: import('pg').Pool
): Promise<IngestResult> {
  const pool = externalPool || getPool();
  const shouldClosePool = !externalPool;

  try {
    // content_hash 계산 (SHA256, 중복 방지)
    const contentHash = crypto
      .createHash('sha256')
      .update(params.content.trim())
      .digest('hex');

    // 중복 확인
    const existing = await pool.query<{ id: string }>(
      'SELECT id FROM knowledge_base WHERE content_hash = $1',
      [contentHash]
    );

    if (existing.rows.length > 0) {
      console.log(`[ingest] 중복 스킵: ${params.title.substring(0, 50)}`);
      return { id: existing.rows[0].id, embedded: false, skipped: true };
    }

    // embedding용 텍스트 조합: title + content (앞 3000자)
    const embeddingText = `[${params.source_type}] ${params.title}\n${params.content.substring(0, 3000)}`;

    // embedding 생성
    let embedding: number[] | null = null;
    let embedded = false;
    try {
      embedding = await embedText(embeddingText);
      embedded = true;
    } catch (e) {
      console.error('[ingest] embedding 생성 실패 (저장은 계속):', e);
    }

    const vectorStr = embedding ? `[${embedding.join(',')}]` : null;

    // INSERT
    const result = await pool.query<{ id: string }>(
      `INSERT INTO knowledge_base
         (source_type, source_path, source_date, task_id, title, content, content_hash, metadata, embedding)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::vector)
       RETURNING id`,
      [
        params.source_type,
        params.source_path || null,
        params.source_date || null,
        params.task_id || null,
        params.title,
        params.content,
        contentHash,
        params.metadata ? JSON.stringify(params.metadata) : null,
        vectorStr,
      ]
    );

    const id = result.rows[0].id;
    console.log(`[ingest] 저장 완료: ${id} | ${params.source_type} | ${params.title.substring(0, 50)}`);

    return { id, embedded, skipped: false };

  } finally {
    if (shouldClosePool) {
      await pool.end();
    }
  }
}

/**
 * CLI 실행:
 *   node dist/ingest-document.js --type report --title "제목" --content "내용" [옵션]
 */
async function main(): Promise<void> {
  const args = process.argv.slice(2);

  if (args.length === 0 || args.includes('--help')) {
    console.log(`사용법: node dist/ingest-document.js [옵션]

필수:
  --type <type>        report|instruction|plan|obsidian|compact_backup|decision|other
  --title <text>       문서 제목
  --content <text>     문서 내용 (또는 --content-stdin으로 stdin 입력)

선택:
  --content-stdin      stdin에서 content 읽기 (파이프 사용 시)
  --source-path <path> 원본 파일 경로
  --source-date <date> 문서 날짜 (ISO 8601)
  --task-id <uuid>     관련 태스크 ID

예시:
  node dist/ingest-document.js --type report --title "일일 보고" --content "오늘 완료한 작업..."
  cat report.md | node dist/ingest-document.js --type plan --title "RAG v1 계획" --content-stdin
`);
    process.exit(0);
  }

  function getArg(name: string): string | undefined {
    const idx = args.indexOf(`--${name}`);
    if (idx === -1 || idx + 1 >= args.length) return undefined;
    return args[idx + 1];
  }

  const type = getArg('type') as IngestParams['source_type'];
  const title = getArg('title');
  let content = getArg('content');
  const useStdin = args.includes('--content-stdin');

  // stdin 읽기
  if (useStdin) {
    const chunks: Buffer[] = [];
    for await (const chunk of process.stdin) {
      chunks.push(chunk as Buffer);
    }
    content = Buffer.concat(chunks).toString('utf-8').trim();
  }

  if (!type || !title || !content) {
    console.error('필수 인자 누락: --type, --title, --content (또는 --content-stdin)');
    process.exit(1);
  }

  const result = await ingestDocument({
    source_type: type,
    title,
    content,
    source_path: getArg('source-path'),
    source_date: getArg('source-date'),
    task_id: getArg('task-id'),
  });

  if (result.skipped) {
    console.log(`결과: 중복 스킵 (id=${result.id})`);
    process.exit(0);
  }

  console.log(`결과: id=${result.id}, embedded=${result.embedded}`);
  process.exit(result.embedded ? 0 : 1);
}

const isMainModule = process.argv[1]?.endsWith('ingest-document.js');
if (isMainModule) {
  main().catch((err) => {
    console.error('Fatal error:', err);
    process.exit(1);
  });
}

Step 2: package.json scripts 추가

"ingest": "node dist/ingest-document.js"

Step 3: 빌드 확인

npm run build

Expected: 오류 없음

Step 4: 동작 테스트

node dist/ingest-document.js \
  --type report \
  --title "RAG v1.0 구현 테스트" \
  --content "v1.0 구현을 위한 테스트 문서입니다. knowledge_base 테이블에 저장됩니다."

Expected:

[ingest] 저장 완료: <uuid> | report | RAG v1.0 구현 테스트
결과: id=<uuid>, embedded=true

같은 명령 재실행 시:

[ingest] 중복 스킵: RAG v1.0 구현 테스트
결과: 중복 스킵 (id=<uuid>)

Step 5: Commit

git add src/ingest-document.ts package.json
git commit -m "feat: ingest-document.ts — knowledge_base 단건 저장 + embedding"

Task 4: ingest-directory.ts — 디렉토리 .md 파일 배치 저장

목적: 지정 디렉토리 내 모든 .md 파일을 재귀적으로 읽어 knowledge_base에 저장한다. content_hash로 이미 저장된 파일은 자동 스킵.

Files:

  • Create: src/ingest-directory.ts

Step 1: ingest-directory.ts 생성

// src/ingest-directory.ts
import fs from 'fs';
import path from 'path';
import { getPool } from './config.js';
import { ingestDocument, IngestParams } from './ingest-document.js';

type SourceType = IngestParams['source_type'];

interface IngestDirResult {
  saved: number;
  skipped: number;
  errors: number;
}

/**
 * 디렉토리 내 .md 파일을 재귀 탐색하여 knowledge_base에 일괄 저장.
 * 중복(content_hash)은 자동 스킵.
 *
 * @param dirPath   - 탐색할 디렉토리 절대/상대 경로
 * @param sourceType - 저장할 source_type
 */
export async function ingestDirectory(
  dirPath: string,
  sourceType: SourceType = 'plan'
): Promise<IngestDirResult> {
  const pool = getPool();
  const result: IngestDirResult = { saved: 0, skipped: 0, errors: 0 };

  try {
    const absDir = path.resolve(dirPath);

    if (!fs.existsSync(absDir)) {
      console.error(`[ingest-dir] 디렉토리 없음: ${absDir}`);
      return result;
    }

    const mdFiles = collectMdFiles(absDir);
    console.log(`[ingest-dir] ${mdFiles.length}개 .md 파일 발견 (${absDir})`);

    for (const filePath of mdFiles) {
      try {
        const content = fs.readFileSync(filePath, 'utf-8').trim();
        if (!content) {
          result.skipped++;
          continue;
        }

        // 파일명을 제목으로 사용 (확장자 제거)
        const title = path.basename(filePath, '.md');

        // 파일 수정일
        const stat = fs.statSync(filePath);
        const sourceDate = stat.mtime;

        const ingestResult = await ingestDocument(
          {
            source_type: sourceType,
            title,
            content,
            source_path: filePath,
            source_date: sourceDate,
          },
          pool
        );

        if (ingestResult.skipped) {
          result.skipped++;
        } else {
          result.saved++;
        }

        // API 레이트 제한 방지 (500ms 간격)
        await new Promise(r => setTimeout(r, 500));

      } catch (err) {
        console.error(`[ingest-dir] 오류: ${filePath}`, err);
        result.errors++;
      }
    }

    console.log(`[ingest-dir] 완료: 저장=${result.saved}, 스킵=${result.skipped}, 오류=${result.errors}`);
    return result;

  } finally {
    await pool.end();
  }
}

/**
 * 디렉토리를 재귀 탐색하여 .md 파일 경로 목록 반환
 */
function collectMdFiles(dirPath: string): string[] {
  const files: string[] = [];

  const entries = fs.readdirSync(dirPath, { withFileTypes: true });
  for (const entry of entries) {
    const fullPath = path.join(dirPath, entry.name);
    if (entry.isDirectory()) {
      files.push(...collectMdFiles(fullPath));
    } else if (entry.isFile() && entry.name.endsWith('.md')) {
      files.push(fullPath);
    }
  }

  return files;
}

/**
 * CLI 실행:
 *   node dist/ingest-directory.js <디렉토리 경로> [source_type]
 */
async function main(): Promise<void> {
  const dirPath = process.argv[2];
  const sourceType = (process.argv[3] || 'plan') as SourceType;

  if (!dirPath) {
    console.log('사용법: node dist/ingest-directory.js <디렉토리 경로> [source_type]');
    console.log('예시:');
    console.log('  node dist/ingest-directory.js /path/to/docs/plans plan');
    console.log('  node dist/ingest-directory.js /path/to/ObsidianVault/projects obsidian');
    process.exit(1);
  }

  const result = await ingestDirectory(dirPath, sourceType);
  process.exit(result.errors > 0 ? 1 : 0);
}

const isMainModule = process.argv[1]?.endsWith('ingest-directory.js');
if (isMainModule) {
  main().catch((err) => {
    console.error('Fatal error:', err);
    process.exit(1);
  });
}

Step 2: package.json scripts 추가

"ingest-dir": "node dist/ingest-directory.js"

Step 3: 빌드 및 테스트

npm run build

# docs/plans/2026/02/26/ 디렉토리 테스트 (소규모)
node dist/ingest-directory.js \
  "/Users/nbs22/(Claude)/(claude).projects/business-builder/docs/plans/2026/02/26" \
  plan

Expected: 해당 디렉토리의 .md 파일들이 저장됨 (중복은 스킵)

Step 4: Commit

git add src/ingest-directory.ts package.json
git commit -m "feat: ingest-directory.ts — 디렉토리 .md 배치 저장"

Task 5: search-kb.ts — knowledge_base + task_comments 통합 검색

목적: knowledge_base와 task_comments를 통합 검색하는 CLI. 기존 search.ts는 task_comments만 검색하므로 신규 통합 검색이 필요하다.

Files:

  • Create: src/search-kb.ts

Step 1: search-kb.ts 생성

// src/search-kb.ts
import { getPool } from './config.js';
import { embedText } from './embeddings.js';

interface KBResult {
  id: string;
  source: 'knowledge_base' | 'task_comments';
  source_type: string;
  source_path: string | null;
  title: string;
  content_preview: string;
  similarity: number;
  created_at: string;
}

/**
 * knowledge_base + task_comments를 통합 유사도 검색.
 *
 * @param query  - 자연어 검색 쿼리
 * @param topK   - 반환 결과 수 (기본 5)
 * @param source - 검색 대상 ('all' | 'kb' | 'comments', 기본 'all')
 */
export async function searchKB(
  query: string,
  topK: number = 5,
  source: 'all' | 'kb' | 'comments' = 'all'
): Promise<KBResult[]> {
  const pool = getPool();

  try {
    const queryEmbedding = await embedText(query);
    const vectorStr = `[${queryEmbedding.join(',')}]`;

    let sql: string;

    if (source === 'kb') {
      sql = `
        SELECT
          id,
          'knowledge_base'::text AS source,
          source_type,
          source_path,
          title,
          LEFT(content, 300) AS content_preview,
          1 - (embedding <=> $1::vector) AS similarity,
          created_at::text
        FROM knowledge_base
        WHERE embedding IS NOT NULL
        ORDER BY embedding <=> $1::vector
        LIMIT $2
      `;
    } else if (source === 'comments') {
      sql = `
        SELECT
          c.id,
          'task_comments'::text AS source,
          c.type AS source_type,
          NULL AS source_path,
          COALESCE(t.title, '(태스크 없음)') AS title,
          LEFT(c.content, 300) AS content_preview,
          1 - (c.embedding <=> $1::vector) AS similarity,
          c.created_at::text
        FROM task_comments c
        LEFT JOIN tasks t ON c.task_id = t.id
        WHERE c.embedding IS NOT NULL
        ORDER BY c.embedding <=> $1::vector
        LIMIT $2
      `;
    } else {
      // 통합: UNION ALL 후 정렬
      sql = `
        SELECT * FROM (
          SELECT
            id,
            'knowledge_base'::text AS source,
            source_type,
            source_path,
            title,
            LEFT(content, 300) AS content_preview,
            1 - (embedding <=> $1::vector) AS similarity,
            created_at::text
          FROM knowledge_base
          WHERE embedding IS NOT NULL

          UNION ALL

          SELECT
            c.id,
            'task_comments'::text AS source,
            c.type AS source_type,
            NULL AS source_path,
            COALESCE(t.title, '(태스크 없음)') AS title,
            LEFT(c.content, 300) AS content_preview,
            1 - (c.embedding <=> $1::vector) AS similarity,
            c.created_at::text
          FROM task_comments c
          LEFT JOIN tasks t ON c.task_id = t.id
          WHERE c.embedding IS NOT NULL
        ) combined
        ORDER BY similarity DESC
        LIMIT $2
      `;
    }

    const result = await pool.query<KBResult>(sql, [vectorStr, topK]);
    return result.rows;

  } finally {
    await pool.end();
  }
}

function formatKBResults(query: string, results: KBResult[]): void {
  console.log(`\n${'='.repeat(70)}`);
  console.log(`검색 쿼리: "${query}"`);
  console.log(`결과: ${results.length}건`);
  console.log(`${'='.repeat(70)}\n`);

  if (results.length === 0) {
    console.log('검색 결과 없음');
    return;
  }

  results.forEach((r, i) => {
    const pct = (r.similarity * 100).toFixed(1);
    const sourceLabel = r.source === 'knowledge_base' ? `📄 KB` : `💬 코멘트`;
    console.log(`--- [${i + 1}] 유사도: ${pct}% | ${sourceLabel} | ${r.source_type} ---`);
    console.log(`제목: ${r.title}`);
    if (r.source_path) {
      console.log(`경로: ${r.source_path}`);
    }
    console.log(`내용: ${r.content_preview}${r.content_preview.length >= 300 ? '...' : ''}`);
    console.log(`날짜: ${r.created_at}`);
    console.log();
  });
}

async function main(): Promise<void> {
  const query = process.argv[2];
  const topK = parseInt(process.argv[3] || '5', 10);
  const source = (process.argv[4] || 'all') as 'all' | 'kb' | 'comments';

  if (!query) {
    console.log('사용법: node dist/search-kb.js "쿼리" [결과수] [all|kb|comments]');
    console.log('예시:');
    console.log('  node dist/search-kb.js "AI 뉴스레터 의사결정" 5');
    console.log('  node dist/search-kb.js "NeonDB 비용" 3 kb');
    console.log('  node dist/search-kb.js "컨텍스트 소실" 5 comments');
    process.exit(1);
  }

  try {
    const results = await searchKB(query, topK, source);
    formatKBResults(query, results);
  } catch (err) {
    console.error('검색 오류:', err);
    process.exit(1);
  }
}

main().catch((err) => {
  console.error('Fatal error:', err);
  process.exit(1);
});

Step 2: package.json scripts 추가

"search-kb": "node dist/search-kb.js"

Step 3: 빌드 및 테스트

npm run build
node dist/search-kb.js "RAG 지식베이스" 5

Expected: knowledge_base + task_comments 통합 결과 5건 출력 (Task 3에서 저장한 테스트 문서 포함)

Step 4: Commit

git add src/search-kb.ts package.json
git commit -m "feat: search-kb.ts — knowledge_base + task_comments 통합 검색"

Task 6: scripts/rag-save-report.sh — vice-reply.sh 연동 wrapper

목적: vice-reply.sh에서 호출 가능한 얇은 bash wrapper. 보고 내용을 knowledge_base에 저장한다.

Files:

  • Create: scripts/rag-save-report.sh

주의: 이 스크립트는 business-builder 루트scripts/ 디렉토리에 생성한다. (RAG 프로젝트의 scripts/가 아님)

Step 1: scripts/rag-save-report.sh 생성

경로: /Users/nbs22/(Claude)/(claude).projects/business-builder/scripts/rag-save-report.sh

#!/bin/bash
# RAG 보고서 저장 wrapper — vice-reply.sh에서 호출
# 사용법: ./scripts/rag-save-report.sh "제목" "내용" [source_type] [tag]
#
# 실패해도 조용히 종료 (vice-reply.sh의 주요 흐름을 방해하지 않음)

TITLE="${1:-}"
CONTENT="${2:-}"
SOURCE_TYPE="${3:-report}"
TAG="${4:-report}"

if [ -z "$TITLE" ] || [ -z "$CONTENT" ]; then
  exit 0  # 내용 없으면 조용히 종료
fi

RAG_DIR="/Users/nbs22/(Claude)/(claude).projects/business-builder/projects/rag-knowledge-base"

if [ ! -d "$RAG_DIR" ]; then
  exit 0
fi

export PATH="/Users/nbs22/.nvm/versions/node/v22.12.0/bin:$PATH"

# .env 로드 (RAG_DB_URL, OPENAI_API_KEY)
if [ -f "$RAG_DIR/.env" ]; then
  set -a
  source "$RAG_DIR/.env"
  set +a
fi

# ingest-document CLI 호출 (에러 무시)
cd "$RAG_DIR" && node dist/ingest-document.js \
  --type "$SOURCE_TYPE" \
  --title "$TITLE" \
  --content "$CONTENT" \
  --source-path "report:$TAG:$(date +%Y-%m-%d)" \
  2>/dev/null || true

exit 0

Step 2: 실행 권한 설정

chmod +x /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder/scripts/rag-save-report.sh

Step 3: 단독 테스트

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder
./scripts/rag-save-report.sh "테스트 보고서" "rag-save-report.sh 동작 테스트 완료" report test

Expected: 에러 없이 종료 (knowledge_base에 저장됨)

검증:

cd projects/rag-knowledge-base
node dist/search-kb.js "rag-save-report 테스트" 3 kb

Step 4: Commit

git add scripts/rag-save-report.sh
git commit -m "feat: rag-save-report.sh — vice-reply.sh 연동 wrapper"

Task 7: vice-reply.sh — RAG 훅 step 5 추가

목적: vice-reply.sh 실행 시 보고 내용이 자동으로 knowledge_base에 저장된다. 기존 4단계(텔레그램/DB/wake/md) 이후 step 5로 추가.

Files:

  • Modify: scripts/vice-reply.sh

Step 1: vice-reply.sh 끝부분 확인

현재 마지막 줄:

echo "[4/4] 로컬 md 백업 완료: memory/jarvis-reports/${FILENAME}"

Step 2: step 5 추가 (마지막 echo 이후에 삽입)

echo "[4/4] 로컬 md 백업 완료: memory/jarvis-reports/${FILENAME}"

# 5) RAG 지식베이스 저장 (실패해도 계속)
REPORT_TITLE="[${TAG}] $(echo "$TEXT" | head -1 | cut -c1-80)"
"${PROJECT_ROOT}/scripts/rag-save-report.sh" "$REPORT_TITLE" "$TEXT" "report" "$TAG" || true
echo "[5/5] RAG 지식베이스 저장 완료"

Step 3: 동작 테스트

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder
./scripts/vice-reply.sh "RAG v1.0 훅 연동 테스트. vice-reply.sh에서 자동 RAG 저장 확인." "test"

Expected: [5/5] RAG 지식베이스 저장 완료 출력

cd projects/rag-knowledge-base
node dist/search-kb.js "vice-reply RAG 훅 연동" 3 kb

Expected: 방금 저장된 보고서 검색됨

Step 4: Commit

git add scripts/vice-reply.sh
git commit -m "feat: vice-reply.sh RAG 훅 추가 (step 5)"

Task 8: scripts/rag-ingest-docs.sh — docs/plans/ 배치 저장

목적: business-builder/docs/plans/ 하위 모든 .md 파일을 knowledge_base에 배치 저장한다.

Files:

  • Create: scripts/rag-ingest-docs.sh

경로: /Users/nbs22/(Claude)/(claude).projects/business-builder/scripts/rag-ingest-docs.sh

Step 1: rag-ingest-docs.sh 생성

#!/bin/bash
# docs/plans/ 하위 .md 파일을 knowledge_base에 배치 저장
# 사용법: ./scripts/rag-ingest-docs.sh [디렉토리] [source_type]
#
# 예시:
#   ./scripts/rag-ingest-docs.sh                          # 전체 docs/plans/
#   ./scripts/rag-ingest-docs.sh docs/plans/2026/02/26    # 특정 날짜만

set -euo pipefail

PROJECT_ROOT="/Users/nbs22/(Claude)/(claude).projects/business-builder"
RAG_DIR="${PROJECT_ROOT}/projects/rag-knowledge-base"
TARGET_DIR="${1:-${PROJECT_ROOT}/docs/plans}"
SOURCE_TYPE="${2:-plan}"

export PATH="/Users/nbs22/.nvm/versions/node/v22.12.0/bin:$PATH"

# .env 로드
if [ -f "$RAG_DIR/.env" ]; then
  set -a
  source "$RAG_DIR/.env"
  set +a
fi

echo "[rag-ingest-docs] 시작: $TARGET_DIR (type=$SOURCE_TYPE)"

cd "$RAG_DIR"
node dist/ingest-directory.js "$TARGET_DIR" "$SOURCE_TYPE"

echo "[rag-ingest-docs] 완료"

Step 2: 실행 권한 설정

chmod +x /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder/scripts/rag-ingest-docs.sh

Step 3: 실행 테스트

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder

# 오늘 날짜 플랜만 (빠른 테스트)
./scripts/rag-ingest-docs.sh docs/plans/2026/02/26 plan

Expected: 저장 건수 + 중복 스킵 건수 출력

Step 4: Commit

git add scripts/rag-ingest-docs.sh
git commit -m "feat: rag-ingest-docs.sh — docs/plans/ 배치 저장"

Task 9: scripts/rag-ingest-obsidian.sh — ObsidianVault 배치 저장

목적: ObsidianVault/projects/ 하위 문서를 knowledge_base에 배치 저장한다.

Files:

  • Create: scripts/rag-ingest-obsidian.sh

경로: /Users/nbs22/(Claude)/(claude).projects/business-builder/scripts/rag-ingest-obsidian.sh

Step 1: rag-ingest-obsidian.sh 생성

#!/bin/bash
# ObsidianVault/projects/ 하위 .md 파일을 knowledge_base에 배치 저장
# 사용법: ./scripts/rag-ingest-obsidian.sh [하위 경로]
#
# 예시:
#   ./scripts/rag-ingest-obsidian.sh              # 전체 projects/
#   ./scripts/rag-ingest-obsidian.sh plans        # plans/ 만

set -euo pipefail

PROJECT_ROOT="/Users/nbs22/(Claude)/(claude).projects/business-builder"
RAG_DIR="${PROJECT_ROOT}/projects/rag-knowledge-base"
OBSIDIAN_ROOT="/Users/nbs22/ObsidianVault/projects"
SUBPATH="${1:-}"
TARGET_DIR="${OBSIDIAN_ROOT}/${SUBPATH}"

export PATH="/Users/nbs22/.nvm/versions/node/v22.12.0/bin:$PATH"

# .env 로드
if [ -f "$RAG_DIR/.env" ]; then
  set -a
  source "$RAG_DIR/.env"
  set +a
fi

if [ ! -d "$TARGET_DIR" ]; then
  echo "[rag-ingest-obsidian] 경로 없음: $TARGET_DIR"
  exit 1
fi

echo "[rag-ingest-obsidian] 시작: $TARGET_DIR"

cd "$RAG_DIR"
node dist/ingest-directory.js "$TARGET_DIR" "obsidian"

echo "[rag-ingest-obsidian] 완료"

Step 2: 실행 권한 설정

chmod +x /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder/scripts/rag-ingest-obsidian.sh

Step 3: 실행 테스트

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder

# plans/ 하위만 (소규모 테스트)
./scripts/rag-ingest-obsidian.sh plans

Expected: Obsidian plans 문서들이 knowledge_base에 저장됨

Step 4: Commit

git add scripts/rag-ingest-obsidian.sh
git commit -m "feat: rag-ingest-obsidian.sh — ObsidianVault 배치 저장"

Task 10: scripts/rag-compact-backup.sh — Compact 전 세션 백업

목적: 컨텍스트 압축 전 현재 세션의 주요 진행사항/의사결정을 knowledge_base에 저장한다. 파이프 or 인자로 내용을 받는다.

Files:

  • Create: scripts/rag-compact-backup.sh

경로: /Users/nbs22/(Claude)/(claude).projects/business-builder/scripts/rag-compact-backup.sh

Step 1: rag-compact-backup.sh 생성

#!/bin/bash
# Compact 직전 세션 백업 — 현재 세션 핵심 내용을 knowledge_base에 저장
#
# 사용법:
#   ./scripts/rag-compact-backup.sh "세션 요약" [태스크ID]
#   echo "세션 내용" | ./scripts/rag-compact-backup.sh --stdin [태스크ID]
#
# 예시 (자비스 compact 직전 실행):
#   ./scripts/rag-compact-backup.sh "오늘 완료: RAG v1.0 구현. 남은 작업: ObsidianVault 통합"

set -euo pipefail

PROJECT_ROOT="/Users/nbs22/(Claude)/(claude).projects/business-builder"
RAG_DIR="${PROJECT_ROOT}/projects/rag-knowledge-base"

export PATH="/Users/nbs22/.nvm/versions/node/v22.12.0/bin:$PATH"

# .env 로드
if [ -f "$RAG_DIR/.env" ]; then
  set -a
  source "$RAG_DIR/.env"
  set +a
fi

TIMESTAMP=$(date +"%Y-%m-%d %H:%M")
TITLE="[compact_backup] ${TIMESTAMP}"

# stdin 모드
if [ "${1:-}" = "--stdin" ]; then
  CONTENT=$(cat)
  TASK_ID="${2:-}"
else
  CONTENT="${1:-}"
  TASK_ID="${2:-}"
fi

if [ -z "$CONTENT" ]; then
  echo "사용법: ./scripts/rag-compact-backup.sh \"세션 요약\" [태스크ID]"
  echo "       echo \"내용\" | ./scripts/rag-compact-backup.sh --stdin"
  exit 1
fi

TASK_ARG=""
if [ -n "$TASK_ID" ]; then
  TASK_ARG="--task-id $TASK_ID"
fi

echo "[compact-backup] 세션 백업 시작..."

cd "$RAG_DIR"
node dist/ingest-document.js \
  --type compact_backup \
  --title "$TITLE" \
  --content "$CONTENT" \
  --source-path "compact:${TIMESTAMP}" \
  $TASK_ARG

echo "[compact-backup] 완료: ${TITLE}"

Step 2: 실행 권한 설정

chmod +x /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder/scripts/rag-compact-backup.sh

Step 3: 동작 테스트

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder

./scripts/rag-compact-backup.sh \
  "RAG v1.0 구현 완료. Task 1~10 모두 완성. 주요 의사결정: content_hash 중복 방지, UNION ALL 통합 검색, vice-reply.sh 훅 연동."

Expected: [compact-backup] 완료: [compact_backup] 2026-02-26 HH:MM

Step 4: Commit

git add scripts/rag-compact-backup.sh
git commit -m "feat: rag-compact-backup.sh — compact 전 세션 백업"

Task 11: rag-usage.md 스킬 업데이트

목적: 자비스 표준 RAG 운영 프로토콜을 업데이트한다. 새 테이블/명령어/저장 방법을 반영한다.

Files:

  • Modify: .claude/skills/jarvis-only/rag-usage.md

경로: /Users/nbs22/(Claude)/(claude).projects/business-builder/.claude/skills/jarvis-only/rag-usage.md

Step 1: rag-usage.md 전체 교체

# RAG 지식베이스 운영 프로토콜

`projects/rag-knowledge-base/`의 검색 시스템으로 과거 의사결정/계획/보고를 참조한다.

## 검색 명령

```bash
cd /Users/nbs22/(Claude)/(claude).projects/business-builder/projects/rag-knowledge-base

# 통합 검색 (task_comments + knowledge_base)
node dist/search-kb.js "검색 쿼리" [topK] [all|kb|comments]

# 예시
node dist/search-kb.js "AI 뉴스레터 의사결정" 5
node dist/search-kb.js "NeonDB 비용" 3 kb
node dist/search-kb.js "유사 과업 선례" 5 comments

# 구버전 (task_comments 전용)
node dist/search.js "쿼리" [topK]

필수 참조 상황

  • 새 태스크 기획 착수 시 (과거 유사 프로젝트 의사결정)
  • 기술 스택/서비스 선택 시 (이전 동일 선택 여부, 사유 확인)
  • 비용 산출 시 (과거 비용 분석 결과 재활용)
  • holding/drop 판단 시 (유사 태스크 보류/폐기 사유)

불필요 상황 (쓰지 않는다)

  • 단순 DB 조회로 충분한 경우
  • 코드 구현 중 기술적 질문
  • CEO와 실시간 대화 중 즉답
  • 이미 해당 태스크의 comments를 SQL 조회한 경우

제한

  • 1회 검색당 상위 5건 (topK=5)
  • 한 태스크 기획에서 최대 3회 검색
  • 검색 결과는 판단의 참고 자료로만 활용

RAG 저장 방법

자동 저장 (훅)

  • vice-reply.sh 실행 시 → 자동으로 knowledge_base에 저장됨 (step 5)

배치 저장 (초기/정기)

cd /Users/nbs22/(Claude)/(claude).projects/business-builder

# docs/plans/ 전체
./scripts/rag-ingest-docs.sh

# 특정 날짜만
./scripts/rag-ingest-docs.sh docs/plans/2026/02/26

# ObsidianVault 전체
./scripts/rag-ingest-obsidian.sh

# ObsidianVault plans만
./scripts/rag-ingest-obsidian.sh plans

Compact 전 백업

cd /Users/nbs22/(Claude)/(claude).projects/business-builder

./scripts/rag-compact-backup.sh "현재 세션 요약: 완료된 작업, 남은 작업, 주요 의사결정"

단건 저장

cd projects/rag-knowledge-base

node dist/ingest-document.js \
  --type report \
  --title "문서 제목" \
  --content "내용"

저장 데이터 구조

source_type내용
reportvice-reply.sh 보고서
instructionCEO 지시사항
plandocs/plans/ 기획서
obsidianObsidianVault 문서
compact_backupCompact 전 세션 백업
decision독립적 의사결정 기록

**Step 2: Commit**

```bash
git add .claude/skills/jarvis-only/rag-usage.md
git commit -m "docs: rag-usage.md 스킬 v1.0 업데이트 — knowledge_base + 자동 저장 프로토콜"

Task 12: 초기 데이터 일괄 저장 (실행)

목적: 기존 docs/plans/ 전체 + ObsidianVault/projects/ 전체를 knowledge_base에 저장한다. 일회성 실행.

주의: OpenAI API 비용 발생. docs/plans/ ≈ 15개 파일 × $0.0002 ≈ $0.003. ObsidianVault ≈ 수십~수백 파일. 비용 최소화를 위해 plans/만 먼저 저장 권장.

Step 1: docs/plans/ 전체 저장

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder
./scripts/rag-ingest-docs.sh

Step 2: 검색 테스트

cd projects/rag-knowledge-base
node dist/search-kb.js "KoreaAI Hub 계획" 5 kb
node dist/search-kb.js "결제 시스템 의사결정" 5

Step 3: ObsidianVault plans 저장 (선택)

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder
./scripts/rag-ingest-obsidian.sh plans
./scripts/rag-ingest-obsidian.sh strategy

Step 4: Commit

# 코드 변경 없음. git push만.
git push origin main

완료 체크리스트

  • Task 1: config.ts RAG_DB_URL 지원, .env 업데이트
  • Task 2: knowledge_base 테이블 생성 완료
  • Task 3: ingest-document.ts 빌드 + 단건 저장 동작 확인
  • Task 4: ingest-directory.ts 빌드 + docs/plans/2026/02/26 테스트
  • Task 5: search-kb.ts 빌드 + 통합 검색 동작 확인
  • Task 6: rag-save-report.sh 단독 실행 테스트
  • Task 7: vice-reply.sh 훅 추가 + 보고 후 검색으로 저장 확인
  • Task 8: rag-ingest-docs.sh 실행 권한 + 동작 확인
  • Task 9: rag-ingest-obsidian.sh 실행 권한 + 동작 확인
  • Task 10: rag-compact-backup.sh 동작 확인
  • Task 11: rag-usage.md 스킬 업데이트
  • Task 12: 초기 데이터 일괄 저장 + 검색 품질 확인

주요 설계 결정

결정이유
knowledge_base 별도 테이블task_comments와 스키마 분리 → 범용 문서 저장 가능
content_hash 중복 방지배치 저장 반복 실행 시 중복 embedding 방지
UNION ALL 통합 검색단일 쿼리로 양쪽 테이블 검색 (인덱스 각각 활용)
vice-reply.sh step 5보고 생성 = 저장 원칙. 별도 훅 없이 기존 흐름에 편승
embedding 실패 시 저장 계속API 오류가 데이터 손실로 이어지지 않도록
500ms 배치 간격OpenAI rate limit 준수 (tier 1: 3000 RPM)
plans/2026/02/26/rag-v1-implementation.md