# CONCIERGE — 개인 AI 비서 구현 플랜

> 작성: 2026-04-08
> 대상 머신: 사무실 우분투 (`park-Uranus-Series`, Tailscale `100.126.230.80`)
> 호스팅: systemd service, 24시간 상시 가동

---

## 1. 목표

사무실 우분투에서 항상 돌아가는 **개인용 Discord 비서**.
자연어로 일정/리마인더/뉴스 구독 등을 지시하면 봇이 디스코드 인프라까지 스스로 구성하고 관리한다.

### 왜 이렇게 짓는가

- **오픈클로 이슈 회피**: SaaS 에이전트(오픈클로 등)는 RCE, API키 평문 저장, 공급망 스킬 등 보안 취약점 심각. 셀프호스팅 + Tailscale 내부망 = 외부 공격면 **0**
- **추가 과금 0원**: `claude -p` headless CLI 를 엔진으로 사용 → 기존 Claude Code 구독 재사용. API 별도 결제 X
- **검증된 패턴 재활용**: `~/agent-pipeline/engine/src/agents/claude-code-client.ts` 가 이미 `claude -p --output-format stream-json --resume <session>` 패턴으로 동작. 이걸 참고
- **디스코드 = UI + 저장소 + 알림 통합**: 별도 웹 프론트 필요 없음. 모바일은 디스코드 앱

---

## 2. 핵심 기능 (MVP)

| # | 기능 | 예시 |
|---|---|---|
| 1 | 자연어 대화 인터페이스 | `@concierge 내일 일정 뭐 있어?` |
| 2 | 일정/리마인더 등록 | `@concierge 내일 오후 3시 치과` → 자동 파싱 + 등록 |
| 3 | 시간 되면 푸시 | 약속 5분 전 DM 또는 지정 채널 멘션 |
| 4 | 뉴스 구독 | `@concierge 매일 아침 8시에 AI 뉴스 헤드라인 5개` |
| 5 | 채널 자동 구성 | `@concierge 일정관리 탭 만들어줘` → 봇이 `#concierge-schedule` 생성하고 DB 에 매핑 저장 |
| 6 | 상태 리포트 | `@concierge 오늘 뭐 해야 해?` → 오늘자 일정 + 리마인더 요약 |

### Post-MVP 후보

- 지출 트래킹 (`@concierge 오늘 커피 5000원 썼어`)
- 메모 저장 (`@concierge 이거 기억해둬: ...`)
- RSS 구독 관리
- 날씨 브리핑 (기상청 API, 이미 invyone 에서 키 있음)
- 비트코인/주식 가격 알림
- 도커 컨테이너 헬스체크 → 이상 시 알림 (사무실 서버 모니터링)

---

## 3. 아키텍처

```
┌──────────────────────────────────────────────────────────┐
│  Discord (사용자 인터페이스)                              │
│  ├── #concierge-chat    (대화용)                          │
│  ├── #concierge-schedule (자동 생성된 일정 채널)          │
│  ├── #concierge-news    (뉴스 푸시)                       │
│  └── DM (개인 알림)                                       │
└──────────────┬───────────────────────────────────────────┘
               │ WebSocket (gateway) + REST
               │
┌──────────────▼───────────────────────────────────────────┐
│  concierge (Node.js + TypeScript, systemd)                │
│  ┌─────────────────┐  ┌─────────────────────────────────┐│
│  │  Discord Layer  │  │  Intent Router                  ││
│  │  (discord.js)   │──▶  ┌─ 봇 멘션                     ││
│  │  - 메시지 수신   │  │  └─ 채널별 자동 파싱 룰         ││
│  │  - 채널 관리    │  └──────────┬──────────────────────┘│
│  └─────────────────┘             │                       │
│                                  ▼                       │
│  ┌─────────────────────────────────────────────────────┐ │
│  │  Claude Engine (claude -p headless)                  │ │
│  │  - spawn claude --model sonnet --output-format       │ │
│  │    stream-json --resume <session>                    │ │
│  │  - 시스템 프롬프트 + tool definitions 전달            │ │
│  │  - 응답에서 tool_use 블록 추출                       │ │
│  └────────────────┬────────────────────────────────────┘ │
│                   │                                      │
│        ┌──────────┴──────────┬──────────┬──────────┐     │
│        ▼                     ▼          ▼          ▼     │
│  ┌──────────┐         ┌──────────┐ ┌────────┐ ┌────────┐│
│  │ Schedule │         │ Channel  │ │ News   │ │ Memory ││
│  │ Tools    │         │ Tools    │ │ Tools  │ │ Tools  ││
│  └────┬─────┘         └────┬─────┘ └───┬────┘ └───┬────┘│
│       └───────┬────────────┴───────────┴──────────┘     │
│               ▼                                          │
│  ┌──────────────────────────────────────────────────┐   │
│  │  SQLite (better-sqlite3)                          │   │
│  │  - schedules, reminders, news_subs, channels_map  │   │
│  └──────────────────────────────────────────────────┘   │
│                                                          │
│  ┌──────────────────────────────────────────────────┐   │
│  │  Scheduler (node-cron)                            │   │
│  │  - 매 분 DB 폴링, 도래한 일정 → Discord 푸시      │   │
│  │  - 뉴스 구독 시간별 크론 등록                     │   │
│  └──────────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────────┘
              │
              └──► claude CLI (host 의 ~/.claude/ 인증 사용)
```

### 통신 방식

- **자연어 입력 → Intent Router → Claude Engine → Tool 호출 → DB/Discord 액션**
- Claude 는 tool use (function calling) 로 어떤 도구 쓸지 선택
- 도구 결과를 Claude 에 다시 전달 → 최종 응답 생성 → Discord 에 전송

### 세션 관리

- 사용자별(또는 채널별)로 `claude_session_id` 유지
- 첫 호출: `claude -p --output-format stream-json "..."` → 응답의 session_id 저장
- 이후 호출: `claude --resume <session_id> -p --output-format stream-json "..."` → 문맥 유지 + cold start 회피
- 세션은 N시간 idle 시 만료 (agent-pipeline 과 동일 패턴)

---

## 4. 데이터 모델 (SQLite)

```sql
-- 일정/리마인더
CREATE TABLE schedules (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  title         TEXT NOT NULL,
  description   TEXT,
  due_at        INTEGER NOT NULL,        -- unix ms
  remind_before INTEGER DEFAULT 300000,  -- 5분 전 (ms)
  notify_channel TEXT,                    -- 디스코드 채널 id, NULL 이면 DM
  user_id       TEXT NOT NULL,            -- 디스코드 user id
  status        TEXT NOT NULL DEFAULT 'pending', -- pending / notified / done / cancelled
  created_at    INTEGER NOT NULL,
  created_by_msg_id TEXT                  -- 원본 디스코드 메시지 id (참조용)
);

CREATE INDEX idx_schedules_due ON schedules(due_at, status);

-- 뉴스 구독
CREATE TABLE news_subscriptions (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  keyword       TEXT NOT NULL,
  cron_expr     TEXT NOT NULL,            -- '0 8 * * *' 매일 아침 8시
  max_items     INTEGER DEFAULT 5,
  channel_id    TEXT NOT NULL,            -- 어느 채널에 푸시할지
  user_id       TEXT NOT NULL,
  last_run_at   INTEGER,
  created_at    INTEGER NOT NULL
);

-- 채널 컨벤션 매핑 ("앞으론 여기에서 받아와" 명령 결과)
CREATE TABLE channel_conventions (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  purpose       TEXT NOT NULL UNIQUE,     -- 'schedule' | 'news' | 'memo' | ...
  channel_id    TEXT NOT NULL,
  auto_parse    INTEGER DEFAULT 0,        -- 1 이면 채널의 모든 메시지 자동 파싱
  created_at    INTEGER NOT NULL
);

-- Claude 세션 캐시 (사용자별 대화 연속성)
CREATE TABLE claude_sessions (
  scope         TEXT PRIMARY KEY,         -- 'user:<discord_id>' 또는 'channel:<id>'
  session_id    TEXT NOT NULL,
  last_used_at  INTEGER NOT NULL
);

-- 메모 (post-MVP)
CREATE TABLE memos (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  content       TEXT NOT NULL,
  tags          TEXT,                     -- comma-separated
  user_id       TEXT NOT NULL,
  created_at    INTEGER NOT NULL
);
```

---

## 5. 자연어 → 도구 라우팅

### 시스템 프롬프트 (Claude 에게 주는 것)

```
너는 개인 비서 봇 concierge 다. 사용자가 디스코드에서 자연어로 지시하면
적절한 도구를 호출해서 작업을 수행하고, 결과를 한국어로 친근하게 답한다.

현재 시간: {ISO 8601}
현재 채널: #{channel_name} (id: {channel_id})
사용자: {username} (id: {user_id})

사용 가능한 도구:
- add_schedule(title, due_at, remind_before?, notify_channel?)
- list_schedules(from?, to?, status?)
- delete_schedule(id)
- create_discord_channel(name, category?)
- set_channel_convention(purpose, channel_id, auto_parse?)
- subscribe_news(keyword, cron_expr, max_items?, channel_id?)
- list_news_subscriptions()
- unsubscribe_news(id)
- fetch_news_now(keyword, max_items?)
- add_memo(content, tags?)
- search_memo(query)
- get_current_time()
- web_search(query) [claude CLI 내장]

규칙:
1. 시간 표현은 반드시 절대 시각(unix ms)으로 변환해서 add_schedule 호출
   예: "내일 3시" → 내일 15:00 KST → unix ms
2. 사용자가 채널 생성을 요청하면 먼저 create_discord_channel,
   그 다음 set_channel_convention 로 용도 매핑 저장
3. 등록/수정/삭제 후에는 사용자에게 확인 메시지 출력
4. 불명확하면 도구 호출 전에 되묻기
```

### Tool 정의는 MCP 서버로

`agent-pipeline` 의 `mailbox-stdio-bridge.ts` 패턴 참고해서,
concierge 의 도구들을 **stdio MCP 서버**로 감싼다.

```
claude -p \
  --model sonnet-4-6 \
  --output-format stream-json \
  --mcp-config ./mcp-concierge.json \
  --resume <session_id> \
  "<사용자 메시지>"
```

`mcp-concierge.json` 은 로컬 stdio MCP 서버 하나 등록:
```json
{
  "mcpServers": {
    "concierge": {
      "command": "node",
      "args": ["./dist/mcp/concierge-mcp.js"]
    }
  }
}
```

`concierge-mcp.js` 가 add_schedule 등 모든 도구를 MCP 프로토콜로 노출.
Claude 가 호출하면 같은 SQLite/DiscordClient 인스턴스에 연결됨.

---

## 6. 파일 구조

```
/home/park/concierge/
├── package.json
├── tsconfig.json
├── .env                          # DISCORD_TOKEN, GUILD_ID, OWNER_USER_ID, DB_PATH, ...
├── .env.example
├── .gitignore
├── data/
│   └── concierge.db              # SQLite (persistent)
├── logs/                         # pino rotating logs
├── mcp-concierge.json            # MCP config for claude -p
├── src/
│   ├── index.ts                  # 엔트리: discord 로그인 + scheduler 부팅
│   ├── config.ts                 # 환경변수 로드 + validation (zod)
│   ├── logger.ts                 # pino
│   ├── db/
│   │   ├── index.ts              # better-sqlite3 init + 마이그레이션
│   │   ├── schema.ts             # SQL DDL
│   │   └── repositories/
│   │       ├── schedules.ts
│   │       ├── news.ts
│   │       ├── channels.ts
│   │       └── sessions.ts
│   ├── discord/
│   │   ├── client.ts             # discord.js Client 설정
│   │   ├── handlers/
│   │   │   ├── message.ts        # messageCreate → intent router
│   │   │   └── ready.ts
│   │   └── notifier.ts           # 일정/뉴스를 디스코드로 푸시하는 유틸
│   ├── claude/
│   │   ├── client.ts             # claude -p spawn 래퍼 (stream-json parser)
│   │   ├── session.ts            # session_id 관리 (DB-backed)
│   │   ├── prompt.ts             # 시스템 프롬프트 빌더
│   │   └── types.ts              # stream-json 타입
│   ├── mcp/
│   │   └── concierge-mcp.ts      # stdio MCP 서버 (도구 정의)
│   ├── tools/                    # MCP 에서 호출되는 실제 로직
│   │   ├── schedule.ts           # add/list/delete_schedule
│   │   ├── channel.ts            # create_discord_channel, set_convention
│   │   ├── news.ts               # subscribe/list/unsubscribe/fetch_news
│   │   ├── memo.ts               # add_memo/search_memo
│   │   └── time.ts               # get_current_time (KST)
│   ├── scheduler/
│   │   ├── index.ts              # node-cron 등록/해제
│   │   ├── poller.ts             # 매 분 schedules 테이블 폴링
│   │   └── news-runner.ts        # 뉴스 구독 실행기
│   └── news/
│       ├── sources/
│       │   ├── rss.ts            # RSS 피드 파싱
│       │   └── google-news.ts    # Google News 키워드 검색
│       └── formatter.ts          # 디스코드용 embed 포맷
├── deploy/
│   ├── concierge.service         # systemd unit file
│   └── install.sh                # systemd 설치 스크립트
└── README.md
```

---

## 7. 디스코드 구조 (초기 구성)

**서버(Guild)**: 개인 서버 1개 (사용자가 직접 만들고 봇 초대)

**카테고리**: `CONCIERGE` (봇이 자동 생성)

**채널**:
- `#concierge-chat` — 사용자 ↔ 봇 대화 기본 채널
- `#concierge-schedule` — 일정 등록/알림 (auto_parse: true, 메시지 올리면 자동 파싱)
- `#concierge-news` — 뉴스 푸시 전용
- `#concierge-log` — 봇 동작 로그/에러 (디버깅용, 조용히)

**DM**: 개인 알림은 기본적으로 사용자 DM 으로 (중요한 일정)

**권한**: 봇은 `Manage Channels`, `Send Messages`, `Read Message History`, `Mention Everyone` 필요

---

## 8. 스케줄러 동작

### 일정/리마인더 푸시 (매 분 폴링)

```ts
// scheduler/poller.ts
cron.schedule('* * * * *', async () => {
  const now = Date.now()
  const due = db.schedules.findDue(now)  // status=pending AND (due_at - remind_before) <= now
  for (const s of due) {
    await notifier.pushSchedule(s)       // DM 또는 채널 멘션
    db.schedules.markNotified(s.id)
  }
})
```

### 뉴스 구독 (동적 크론 등록)

```ts
// scheduler/news-runner.ts
// 부팅 시 news_subscriptions 전부 로드해서 node-cron 등록
// subscribe_news 도구 호출 시 runtime 에 크론 추가
// unsubscribe_news 호출 시 크론 제거
```

---

## 9. 뉴스 수집

MVP 는 **Google News RSS** 사용 (키 불필요):

```
https://news.google.com/rss/search?q={keyword}&hl=ko&gl=KR&ceid=KR:ko
```

- `rss-parser` 로 파싱
- 최근 N개 아이템 → 제목/링크/요약을 디스코드 embed 로 포맷
- 중복 방지: 최근 푸시한 link 목록을 DB 에 저장(또는 메모리 LRU)

향후 확장:
- 네이버 뉴스 API (키 발급 필요)
- 특정 사이트 RSS 직접 등록
- Claude 로 요약 ("이 기사 3줄로 정리해줘") — 뉴스 푸시 시 자동 요약 옵션

---

## 10. 보안

- **외부 노출 0**: Tailscale 내부망에서만 봇 서버 접근 가능. 봇은 outbound 만 씀 (Discord gateway/REST)
- **.env 파일 권한**: `chmod 600 .env`
- **DB 파일 권한**: `chmod 600 data/concierge.db`
- **MCP 서버 로컬 only**: stdio 통신이라 네트워크 노출 없음
- **디스코드 봇 권한 최소화**: 봇이 참여한 Guild 안에서만 동작. `GUILD_ID` 환경변수로 화이트리스트
- **`OWNER_USER_ID` 체크**: 소유자 외 사용자가 명령해도 봇이 거부 (개인 봇이므로)
- **Claude 세션 격리**: session_id 는 `user:<discord_id>` 스코프. 다른 사용자 문맥 섞임 방지
- **로그 민감정보 마스킹**: 토큰/세션 ID 는 로그에 풀로 안 찍기

---

## 11. 배포 — systemd

### `/etc/systemd/system/concierge.service`

```ini
[Unit]
Description=Concierge — Personal AI Assistant
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=park
WorkingDirectory=/home/park/concierge
ExecStart=/usr/bin/node /home/park/concierge/dist/index.js
Restart=on-failure
RestartSec=5s
StandardOutput=append:/home/park/concierge/logs/stdout.log
StandardError=append:/home/park/concierge/logs/stderr.log
Environment=NODE_ENV=production
EnvironmentFile=/home/park/concierge/.env

# 리소스 제한
MemoryMax=512M
CPUQuota=50%

[Install]
WantedBy=multi-user.target
```

### 운영 명령어

```bash
sudo systemctl enable concierge
sudo systemctl start concierge
sudo systemctl status concierge
journalctl -u concierge -f       # 실시간 로그
sudo systemctl restart concierge
```

---

## 12. 구현 단계 (마일스톤)

| # | 단계 | 완료 기준 | 예상 파일 |
|---|---|---|---|
| M1 | 프로젝트 초기화 + 디스코드 봇 핑/퐁 | 봇 온라인, `@concierge ping` → "pong" | `package.json`, `src/index.ts`, `src/discord/client.ts`, `.env.example` |
| M2 | SQLite + 마이그레이션 | 부팅 시 DB 파일 생성, 모든 테이블 존재 | `src/db/*` |
| M3 | Claude engine 래퍼 (`claude -p` spawn) | 사용자 메시지 → Claude 응답 받아서 디스코드에 답장 | `src/claude/*` |
| M4 | MCP 서버 + 기본 도구 (add/list_schedule) | `@concierge 내일 3시 회의` → DB 저장 + 확인 메시지 | `src/mcp/*`, `src/tools/schedule.ts` |
| M5 | 스케줄러 폴러 → 디스코드 푸시 | 일정 시간 되면 DM/채널 멘션 도달 | `src/scheduler/*`, `src/discord/notifier.ts` |
| M6 | 채널 자동 생성 + 컨벤션 매핑 | `@concierge 일정 채널 만들어줘` → #concierge-schedule 생성 + DB 기록 | `src/tools/channel.ts` |
| M7 | 뉴스 구독 + 푸시 | 매일 정해진 시간에 뉴스 헤드라인 푸시됨 | `src/tools/news.ts`, `src/news/*`, `src/scheduler/news-runner.ts` |
| M8 | systemd 배포 | `systemctl status concierge` active, 재부팅 후 자동 기동 | `deploy/*` |
| M9 | 안정화 | 1주일 간 에러 없이 동작, 로그 정리, README 작성 | `README.md` |

각 마일스톤마다 동작 확인 후 다음 단계로.

---

## 13. 지금 모르는 것 / 나중에 결정할 것

- [ ] Claude Code CLI 가 `--model sonnet-4-6` 정확한 플래그 이름이 맞는지 확인 필요 (`claude --help` 로 검증)
- [ ] `stream-json` 포맷에서 tool_use 블록이 어떻게 표시되는지 agent-pipeline 의 `claude-code-client.ts` 를 자세히 분석
- [ ] 시간대 처리: 사용자 입력은 KST 가정, DB 는 unix ms UTC 저장 → 출력 시 KST 변환
- [ ] 한국어 자연어 날짜 파싱 정확도 (Claude 에게 맡기되 edge case 테스트)
- [ ] 디스코드 봇 생성 절차 (Developer Portal → Bot → token 발급 → 서버 초대) — 사용자 수동 작업 1회 필요

---

## 14. 사용자가 해야 할 수동 작업 (최소)

1. **디스코드 개인 서버 하나 만들기** (없으면)
2. **https://discord.com/developers/applications 에서 봇 어플리케이션 생성 → 토큰 복사**
3. **봇 서버에 초대** (OAuth2 URL 생성 후 브라우저에서 동의)
4. **봇 토큰과 Guild ID 를 사무실 우분투의 `/home/park/concierge/.env` 에 넣기**

그 외 전부 내가 (Claude) 자동화.

---

## 15. 예상 결과물 미리보기 (동작 예시)

```
사용자: @concierge 내일 오후 3시에 치과 예약 있어, 30분 전에 알려줘
봇: ✅ 등록했어요!
    📅 치과 예약
    🕒 2026-04-09 (목) 15:00 KST
    🔔 14:30 에 DM 으로 알림 보낼게요

사용자: @concierge 매일 아침 8시에 AI 관련 뉴스 5개 보내줘
봇: 📰 뉴스 구독 등록!
    키워드: "AI"
    시간: 매일 08:00 KST
    채널: #concierge-news (없으면 만들까요?)

사용자: @concierge 응 만들어줘
봇: 🛠 #concierge-news 채널 만들었어요. 내일 아침부터 거기로 보낼게요.

(다음날 08:00)
#concierge-news:
봇: 📰 AI 뉴스 (2026-04-09)
    1. [오픈클로 보안 패치 배포...](link)
    2. [Anthropic Sonnet 4.6 출시...](link)
    3. ...

(목 14:30, DM)
봇: ⏰ 30분 뒤 치과 예약이에요!
```

---

## 16. 변경 이력

- 2026-04-08: 최초 작성 (gbpark + Claude)
