# ainote — 전체 문서 > AI 네이티브 태스크 + 메모리 + Vault + 다중 디바이스 동기화. MCP 서버. > Source: https://docs.ainote.dev > Generated: 2026-08-11T03:44:19.952Z --- # docs.ainote.dev ainote 의 공식 개발자 문서. VitePress 기반. ## 빠른 시작 ```bash npm install npm run dev # http://localhost:5173 npm run build # llms.txt 자동 생성 + 정적 사이트 빌드 npm run preview # 빌드 결과 로컬에서 확인 ``` ## 구조 ``` docs-site/ ├── .vitepress/ │ └── config.mjs # 8섹션 nav/sidebar + SEO + JSON-LD + sitemap + canonical ├── scripts/ │ └── build-llms.mjs # llms.txt + llms-full.txt 자동 생성 ├── public/ # 정적 자산 (llms.txt 자동 생성됨) │ ├── llms.txt │ └── llms-full.txt ├── index.md # 홈 (hero + features grid + FAQ) ├── guide/ # 시작하기 (5) ├── tasks/ # 태스크 (4) ├── memory/ # 메모리 / Dev Docs (5) ├── vault/ # Vault (5) ├── sync/ # 동기화 (8) ├── mcp/ # MCP 통합 (7) ├── cli/ # CLI (4) ├── reference/ # API 레퍼런스 (28) └── examples/ # 실전 워크플로우 (6) ``` 총 **75 페이지**, 192 KB llms-full.txt. ## 새 페이지 추가 1. 적절한 디렉토리에 `.md` 파일 작성 2. `.vitepress/config.mjs` 의 sidebar 에 항목 추가 3. `npm run dev` 로 확인 4. (배포 시 자동) `npm run build` → llms.txt 갱신 ## 마크다운 주의 - `` 형식의 텍스트는 **반드시 백틱** 으로 감싸야 함 (``) - 그렇지 않으면 VitePress 가 Vue HTML 태그로 파싱 시도 → 빌드 실패 - code block (```) 안은 안전 ## 배포 ### Render Static Site (권장) ```yaml # render.yaml or dashboard type: static_site name: ainote-docs buildCommand: cd docs-site && npm install && npm run build publishPath: docs-site/.vitepress/dist ``` ### 커스텀 도메인 DNS: `docs.ainote.dev` CNAME → `.onrender.com` ## llms.txt 표준 [llmstxt.org](https://llmstxt.org/) 표준 따름: - `/llms.txt` — 페이지 색인 (LLM 이 빠르게 사이트맵 파악) - `/llms-full.txt` — 모든 본문 합친 단일 파일 (LLM context 에 통째로 주입) `scripts/build-llms.mjs` 가 빌드 시 자동 생성. ## 라이선스 MIT — 본 문서 + 코드 모두. --- # Anthropic Claude API — Tool Use Anthropic 의 Messages API 는 `tools` 파라미터를 통해 임의 도구를 LLM 에 노출한다. ainote OpenAPI 3.1 mirror 의 도구 정의를 그대로 변환해 사용 가능 — Anthropic 진영의 모든 직접 API 호출 시나리오 (자체 봇 / SaaS / agentic app / 사내 운영) 가 ainote 를 백엔드로 사용 가능. ## 핵심 아이디어 Anthropic `tools` 형식은 OpenAPI 3.1 paths 와 거의 1:1 매핑된다: | Anthropic `tools[].field` | OpenAPI `paths[/{path}].post.field` | |--------------------------|-------------------------------------| | `name` | path 마지막 segment (예: `handoff_save`) | | `description` | `summary` 또는 `description` | | `input_schema` | `requestBody.content["application/json"].schema` | Anthropic 이 직접 OpenAPI 를 import 하지는 않으므로 한 번 변환만 하면 된다. ## Python 예시 ```python import requests import anthropic # 1. ainote OpenAPI spec 가져오기 spec = requests.get("https://api.ainote.dev/api/mcp/openapi.json").json() # 2. paths -> Anthropic tools 변환 tools = [] for path, ops in spec["paths"].items(): op = ops["post"] name = path.split("/")[-1] # /api/mcp/tools/handoff_save -> handoff_save body_schema = op["requestBody"]["content"]["application/json"]["schema"] tools.append({ "name": name, "description": op.get("description") or op.get("summary", ""), "input_schema": body_schema, }) # 3. Anthropic Messages API 호출 client = anthropic.Anthropic() # ANTHROPIC_API_KEY 환경변수 AINOTE_KEY = "" response = client.messages.create( model="claude-opus-4-7", max_tokens=4096, tools=tools, messages=[{"role": "user", "content": "오늘 마감 태스크 보여줘"}], ) # 4. tool_use 응답 시 ainote 에 호출 for block in response.content: if block.type == "tool_use": tool_name = block.name # 예: "list_tasks" tool_input = block.input # 예: {"due_today": True} r = requests.post( f"https://api.ainote.dev/api/mcp/tools/{tool_name}", json=tool_input, headers={"Authorization": f"McpKey {AINOTE_KEY}"}, ) print(r.json()) ``` ## annotations 활용 — destructive 게이팅 Anthropic API 응답에서 tool_use 가 destructive 한 도구일 때 사용자 confirmation 요구: ```python DESTRUCTIVE = { name for path, ops in spec["paths"].items() if (op := ops["post"]) and (ann := op.get("x-mcp-annotations", {})) and ann.get("destructiveHint") for name in [path.split("/")[-1]] } for block in response.content: if block.type == "tool_use": if block.name in DESTRUCTIVE: # 사용자에게 확인 후 호출 confirm = input(f"⚠️ {block.name} 호출하시겠습니까? (y/n): ") if confirm.lower() != "y": continue # 호출 진행 ... ``` ## Anthropic SDK — TypeScript ```ts import Anthropic from "@anthropic-ai/sdk"; const spec = await fetch("https://api.ainote.dev/api/mcp/openapi.json").then(r => r.json()); const tools = Object.entries(spec.paths).map(([path, ops]: [string, any]) => { const op = ops.post; return { name: path.split("/").pop()!, description: op.description || op.summary || "", input_schema: op.requestBody.content["application/json"].schema, }; }); const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! }); const AINOTE_KEY = process.env.AINOTE_API_KEY!; const msg = await client.messages.create({ model: "claude-opus-4-7", max_tokens: 4096, tools, messages: [{ role: "user", content: "save handoff for this session" }], }); for (const block of msg.content) { if (block.type === "tool_use") { const res = await fetch(`https://api.ainote.dev/api/mcp/tools/${block.name}`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `McpKey ${AINOTE_KEY}`, }, body: JSON.stringify(block.input), }); console.log(await res.json()); } } ``` ## Tool result feedback (multi-turn) Anthropic 가 tool 결과를 본 다음 응답을 만들도록 follow-up 메시지를 추가: ```python # 도구 호출 결과 tool_result = r.json() # ainote 응답 # Anthropic 에 결과 전달 → 다음 응답 생성 followup = client.messages.create( model="claude-opus-4-7", max_tokens=4096, tools=tools, messages=[ {"role": "user", "content": "오늘 마감 태스크 보여줘"}, {"role": "assistant", "content": response.content}, {"role": "user", "content": [ { "type": "tool_result", "tool_use_id": block.id, # 위 tool_use block 의 id "content": str(tool_result), } ]}, ], ) ``` ## MCP HTTP transport vs OpenAPI 변환 — 둘 중 어느 것? | 방식 | 장점 | 단점 | |------|------|------| | **MCP HTTP (`api.ainote.dev/api/mcp`)** | JSON-RPC 표준, tools/list 동적 발견, annotations 그대로 wire | Anthropic API 의 `tools` 파라미터가 MCP 를 직접 이해 못 함 (2026-05 현재) — 별도 어댑터 필요 | | **OpenAPI 변환 (위 예시)** | Anthropic API 의 `tools` 파라미터에 한 번 변환만 하면 통합 끝. annotations 는 `x-mcp-annotations` 로 직접 조회 가능 | OpenAPI spec 변경 시 재변환 필요 (캐시 + 주기적 fetch) | **추천**: 대부분의 경우 OpenAPI 변환이 단순. MCP HTTP 는 Claude Code / Claude Desktop 같은 MCP-native 클라이언트에만 사용. ## 자주 묻는 문제 **"input_schema validation error"** — Anthropic 의 `input_schema` 는 OpenAPI 의 `requestBody.content["application/json"].schema` 와 거의 같지만, 일부 OpenAPI-only 필드 (`example`, `examples`, `x-*` extension) 는 dropped. 변환 시 안전. **"Rate limit 429"** — Anthropic 측 API rate limit + ainote 측 fair-use 한도 둘 다 체크. Phase 3 후 `X-RateLimit-*` 헤더로 ainote 측 quota 노출 예정. **"맥OS Keychain 에서 key 어떻게?"** — Python `keyring` 라이브러리 + macOS `security find-generic-password -s @ainote/cli`. → [전체 도구 매트릭스](/reference/annotations) · [LangChain 진영](/agents/langchain) --- # Claude Code — HTTP MCP (권장) Claude Code 는 stdio 와 HTTP 두 MCP transport 를 모두 지원한다. ainote 는 **HTTP transport 가 권장 경로** — npm 패키지 설치 불필요, 즉시 최신 도구 노출. ## 설정 (1단계) `~/.claude.json` 의 `mcpServers` 에 추가: ```json { "mcpServers": { "ainote": { "type": "http", "url": "https://api.ainote.dev/api/mcp", "headers": { "Authorization": "McpKey " } } } } ``` ::: warning `type: "http"` 필수 원격 MCP 서버는 `type: "http"` 필드 명시 안 하면 Claude Code 의 user-level MCP 로더가 **전체 mcpServers 블록을 스키마 검증 실패로 거부**한다. 한 항목 누락 시 다른 모든 MCP 서버도 같이 안 뜸. 2026-04-15 ainote 자체 사고 사례. ::: ## MCP key 발급 세 방법 중 하나: ### (a) Claude 안에서 즉시 (가장 쉬움) 임시로 key 없이 등록: ```json { "mcpServers": { "ainote": { "type": "http", "url": "https://api.ainote.dev/api/mcp" } } } ``` Claude Code 재시작 후 입력: > "ainote 가입시켜줘 — 이메일 me@example.com / 비밀번호 abcd1234" → `signup_and_get_key` 가 호출돼서 key 가 응답에 표시된다. 그 key 를 위 config 의 `Authorization` header 에 추가하고 다시 재시작. ### (b) CLI device flow ```bash npx @ainote/mcp login ``` 브라우저로 RFC 8628 device authorization grant → key 가 OS Keychain 에 저장. `~/.claude.json` 에 별도 헤더 안 넣어도 stdio mode 에서 자동 사용. ### (c) 웹 [app.ainote.dev](https://app.ainote.dev) → 가입 → Settings → MCP keys → Generate. ## 동작 확인 Claude Code 재시작 후: > "내 ainote 태스크 목록 보여줘" `list_tasks` 가 호출돼서 응답이 표시되면 정상. 도구가 안 보이면: 1. `~/.claude.json` 의 JSON 문법 (콤마 / 따옴표) 확인 2. `claude --debug` 모드로 MCP 로딩 로그 확인 3. `curl https://api.ainote.dev/health` → 200 OK 인지 ## 사용 가능 도구 50+개 도구가 모두 노출된다 (정확한 개수는 계속 늘어난다). 카테고리: - **Tasks (5)**: list_tasks, create_task, update_task, delete_task, list_categories - **Dev Docs (7)**: list_dev_docs, get_dev_doc, create_dev_doc, update_dev_doc, pull_dev_docs, delete_dev_doc, list_dev_categories - **Onboarding (3)**: signup_and_get_key, login_and_get_key, get_setup_guide - **Vaults (5)**: vault_list, vault_create, vault_clone, vault_connect_status, vault_sync - **Sync (3)**: sync_push, sync_pull, sync_list - **Handoffs (3)**: handoff_save, handoff_list, handoff_get → [전체 도구 매트릭스 + annotations](/reference/annotations) ## Tool annotations 활용 Claude Code 는 `tools/list` 응답의 annotations 를 읽어 자율 호출을 게이트한다. 예: - `destructiveHint: true` 도구 (`delete_task`, `vault_sync`, `handoff_list` 등) → user confirmation 우선 - `idempotentHint: true` 도구 → 타임아웃/네트워크 오류 시 자동 재시도 안전 - `openWorldHint: true` 도구 (`vault_create`, `signup_and_get_key`) → 외부 시스템 호출, 결과 비결정적 가능 ## 자주 묻는 문제 **"ainote MCP 가 도구 목록에 안 보임"** — `type: "http"` 필드 누락. `~/.claude.json` 전체에서 다른 `mcpServers` 도 같이 안 뜨면 99% 이 원인. **"401 Unauthorized"** — `Authorization` header 의 `McpKey ` 접두사 (공백 포함) 확인. `Bearer ...` 아님. **"도구는 보이는데 호출 시 timeout"** — `https://api.ainote.dev/health` 가 응답하는지 확인. Render free tier 가 15분 idle 후 cold start (5-10초). → [Troubleshooting 풀세트](/guide/troubleshooting) ## 다른 진영도? - [Claude Desktop (stdio 전용)](/agents/claude-desktop) - [Cursor / Windsurf](/agents/cursor) - [OpenAI Custom GPT](/agents/openai-custom-gpt) - [LangChain / AutoGen](/agents/langchain) --- # Claude Desktop — stdio MCP Claude Desktop 은 HTTP transport 를 지원하지 않고 stdio 만 지원하므로 npm 어댑터를 거쳐야 한다. `@ainote/mcp` 패키지 (v1.3.0) 가 stdio ↔ HTTP 브릿지 역할. ## 설정 Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS): ```json { "mcpServers": { "ainote": { "command": "npx", "args": ["-y", "@ainote/mcp"], "env": { "AINOTE_API_URL": "https://api.ainote.dev", "AINOTE_API_KEY": "" } } } } ``` ## MCP key 발급 ### 방법 1: CLI device flow (권장 — Keychain 자동 저장) ```bash npx @ainote/mcp login ``` 브라우저로 [app.ainote.dev](https://app.ainote.dev) 가 열리고, 디바이스 코드를 입력하면 token + MCP key 가 OS Keychain 에 저장된다. 이 경우 `env.AINOTE_API_KEY` 안 넣어도 됨 — npm 패키지가 자동으로 Keychain 에서 가져온다. ```json { "mcpServers": { "ainote": { "command": "npx", "args": ["-y", "@ainote/mcp"], "env": { "AINOTE_API_URL": "https://api.ainote.dev" } } } } ``` ### 방법 2: Claude 안에서 가입 빈 config 로 등록 → 재시작 후: > "ainote 가입시켜줘 — me@example.com / abcd1234" 응답에서 key 받아 위 env 에 추가. ### 방법 3: 웹 [app.ainote.dev](https://app.ainote.dev) → Settings → MCP keys → Generate. ## 동작 확인 Claude Desktop 재시작 후 도구 목록 (Search & Tools 아이콘) 에 `ainote` namespace 가 뜨고 26 도구 노출. ``` ainote: list_tasks ainote: create_task ainote: handoff_save ainote: vault_sync ... ``` ## OS Keychain 저장 위치 | OS | Location | |----|----------| | macOS | Keychain Access — service `@ainote/cli` | | Linux | libsecret (gnome-keyring / kwallet) — service `@ainote/cli` | | Windows | Credential Manager — target `@ainote/cli` | | Fallback | `${XDG_CONFIG_HOME:-~/.config}/ainote/credentials.json` (mode `0600`) | ## CLI 명령어 ```bash npx @ainote/mcp login # 로그인 (브라우저) npx @ainote/mcp whoami # 현재 사용자 확인 npx @ainote/mcp logout # revoke + Keychain clear npx @ainote/mcp signup # 신규 가입 (대화형) ``` ## E2E 암호화 (mcp category) `@ainote/mcp` 는 `~/.claude/mcpServers` 와 API key 를 ainote 클라우드에 동기화 시 **age** 로 클라이언트 암호화 후 업로드한다. 서버는 평문을 모름. ```bash brew install age # age 바이너리 npx @ainote/mcp sync init_encryption # 디바이스 keypair 생성 (Keychain 저장) npx @ainote/mcp sync add_recipient # 다른 디바이스 등록 npx @ainote/mcp sync push_claude_mcp_servers # 암호화 푸시 ``` → [encryption 상세](/security/age) ## 다른 진영도? - [Claude Code (HTTP 권장)](/agents/claude-code) - [Cursor / Windsurf](/agents/cursor) - [OpenAI Custom GPT](/agents/openai-custom-gpt) --- # Cursor — MCP (stdio 또는 HTTP) Cursor 는 Claude Desktop 과 동일한 MCP 구조를 사용한다. ## 설정 — Cursor settings → MCP `.cursor/mcp.json` (workspace) 또는 `~/Library/Application Support/Cursor/User/mcp.json` (global): ### Option A: HTTP (권장) ```json { "mcpServers": { "ainote": { "type": "http", "url": "https://api.ainote.dev/api/mcp", "headers": { "Authorization": "McpKey " } } } } ``` ### Option B: stdio (npm) ```json { "mcpServers": { "ainote": { "command": "npx", "args": ["-y", "@ainote/mcp"], "env": { "AINOTE_API_URL": "https://api.ainote.dev", "AINOTE_API_KEY": "" } } } } ``` ## 동작 확인 Cursor 재시작 후 Chat → `@ainote` 또는 자연어로: > "내 ainote 태스크 보여줘" 26 도구가 노출되면 정상. → [MCP key 발급 방법](/agents/claude-code#mcp-key-발급) --- # 에이전트 가이드 ainote 는 **MCP** (JSON-RPC) 와 **OpenAPI 3.1** (REST-style) 두 surface 를 동시에 노출한다. 같은 50+개 도구가 두 프로토콜에서 동일하게 동작한다. ## 어느 가이드를 봐야 하나 | 진영 | 추천 protocol | 가이드 | |------|--------------|--------| | Claude Code | MCP HTTP | [Claude Code](/agents/claude-code) | | Claude Desktop | MCP stdio (npm) | [Claude Desktop](/agents/claude-desktop) | | Cursor IDE | MCP stdio 또는 HTTP | [Cursor](/agents/cursor) | | Windsurf IDE | MCP stdio | [Windsurf](/agents/windsurf) | | ChatGPT (Plus/Team/Enterprise) | OpenAPI Custom GPT Action | [OpenAI Custom GPT](/agents/openai-custom-gpt) | | LangChain / LangGraph | OpenAPI remote tool | [LangChain](/agents/langchain) | | AutoGen / CrewAI | OpenAPI remote tool | [LangChain](/agents/langchain#autogen) | ## 공통 — 한 번만 받으면 되는 것 모든 진영은 **MCP key** 만 필요하다. 발급 방법: 1. **Claude 안에서** (가장 쉬움) — MCP 등록 후 "ainote 가입시켜줘 — 이메일 X / 비번 Y" 한 줄 2. **CLI** — `npx @ainote/mcp signup` (브라우저 RFC 8628 device flow) 3. **웹** — [app.ainote.dev](https://app.ainote.dev) → Settings → Generate MCP key 발급된 key 는 OS Keychain (Claude Desktop / Code) 또는 환경변수 (LangChain / AutoGen) 또는 Custom GPT Action header value (ChatGPT) 에 넣는다. ## 두 protocol 비교 | 항목 | MCP JSON-RPC | OpenAPI 3.1 | |------|--------------|-------------| | 엔드포인트 | `https://api.ainote.dev/api/mcp` | `https://api.ainote.dev/api/mcp/openapi.json` (spec) + `/api/mcp/tools/{name}` (호출) | | 호출 형식 | `{jsonrpc:"2.0", method:"tools/call", params:{name, arguments}}` | `POST /api/mcp/tools/{name}` body = arguments | | 인증 | `Authorization: McpKey ` | 동일 | | 도구 발견 | `tools/list` | `GET /api/mcp/openapi.json` | | 추천 진영 | Anthropic 생태계 | OpenAI / LangChain / 기타 HTTP-first | | Tool annotations | wire 응답 `annotations` 필드 | `x-mcp-annotations` extension | **같은 26 도구**. 같은 백엔드. 같은 결과. 진영만 다르다. → [Dual-protocol 게이트웨이 코드 상세](/reference/dual-protocol) ## Multi-platform — 네이티브 앱들 에이전트가 아닌 네이티브 surface 들도 같은 vault 를 본다: - [iOS / Android](/agents/mobile) — Flutter 앱, App Store / Play Store - [macOS](/agents/macos) — 네이티브 Swift 앱 - [Apple Watch](/agents/apple-watch) — 빠른 태스크 추가 - [Chrome / Safari 확장](/agents/extensions) — 웹 페이지 클립 - [Telegram](/mcp/telegram) — 봇 인터페이스 전부 동일한 사용자 계정 + vault 를 공유. 한 디바이스에서 변경하면 다른 디바이스가 본다. --- # LangChain / LangGraph / AutoGen — OpenAPI remote tool ainote 의 OpenAPI 3.1 mirror 를 Python 에이전트 프레임워크에서 remote tool 로 등록한다. 별도 어댑터 코드 없이 `openapi.json` URL 만 있으면 된다. ## LangChain ```python from langchain_community.tools.openapi.utils.openapi_utils import OpenAPISpec from langchain.agents.agent_toolkits.openapi.toolkit import RequestsToolkit from langchain.utilities.requests import RequestsWrapper spec = OpenAPISpec.from_url("https://api.ainote.dev/api/mcp/openapi.json") headers = {"Authorization": "McpKey "} toolkit = RequestsToolkit( requests_wrapper=RequestsWrapper(headers=headers), allow_dangerous_requests=True, # destructive 도구 (delete_task 등) 허용 시 ) # agent 에서 사용 from langchain.agents import create_openai_tools_agent agent = create_openai_tools_agent(llm, toolkit.get_tools(), prompt) ``` ::: warning destructive 도구 `tool annotations.destructiveHint: true` 인 도구 (`delete_task`, `vault_sync`, `handoff_list` 등) 는 `allow_dangerous_requests=True` 가 필수. production 환경에서는 explicit user consent 흐름을 추가 권장. ::: ## LangGraph (직접 tool wrapping) LangGraph 에서 fine-grained control 이 필요하면 도구를 직접 wrap: ```python import requests from langchain.tools import tool AINOTE = "https://api.ainote.dev/api/mcp/tools" HEADERS = {"Authorization": "McpKey "} @tool def handoff_save(project: str, topic: str, content: str, time: str = None) -> str: """Save a session handoff note. Use `time` (HHMM, KST) to disambiguate multiple handoffs on the same day.""" body = {"project": project, "topic": topic, "content": content} if time: body["time"] = time r = requests.post(f"{AINOTE}/handoff_save", json=body, headers=HEADERS) return r.json() @tool def handoff_get(project: str, topic: str, date: str = None, time: str = None) -> str: """Retrieve a handoff. Omit date for latest matching.""" body = {"project": project, "topic": topic} if date: body["date"] = date if time: body["time"] = time r = requests.post(f"{AINOTE}/handoff_get", json=body, headers=HEADERS) return r.json() ``` 26 도구 전체를 미리 wrap 한 패키지를 contributing 환영 — `langchain-ainote` 또는 `ainote-langchain` 이름으로 PR 받습니다. ## AutoGen ```python import autogen ainote_tool_spec = { "url": "https://api.ainote.dev/api/mcp/openapi.json", "auth": {"type": "header", "name": "Authorization", "value": "McpKey "}, } assistant = autogen.AssistantAgent( name="ainote_assistant", llm_config={"tools": [ainote_tool_spec]}, ) ``` (AutoGen 의 정확한 tool spec 형식은 라이브러리 버전에 따라 다름 — 최신 docs 참조) ## CrewAI CrewAI 는 LangChain 호환이므로 위 LangChain 패턴 그대로 사용: ```python from crewai import Agent from langchain_community.tools.openapi.utils.openapi_utils import OpenAPISpec agent = Agent( role="Note keeper", goal="Manage user's notes and tasks via ainote", tools=[/* LangChain toolkit 결과 */], ) ``` ## 도구 발견 (tools/list 등가물) OpenAPI spec 의 paths 를 enumerate: ```python import requests spec = requests.get("https://api.ainote.dev/api/mcp/openapi.json").json() for path, ops in spec["paths"].items(): op = ops["post"] name = path.split("/")[-1] annotations = op.get("x-mcp-annotations", {}) print(f"{name}: {op['summary']}") print(f" annotations: {annotations}") ``` 50+개 도구 + 각 annotations 출력. ## 안티패턴 — 피할 것 ❌ **annotations 무시하고 모든 도구 자동 호출** — destructive 도구 (`delete_task`, `vault_sync` 등) 가 user consent 없이 실행되면 데이터 손실 가능 ❌ **API key 하드코딩** — env var (`AINOTE_API_KEY`) 또는 secret manager 사용 ❌ **rate limit 무시** — 짧은 시간 안 다수 호출 → 429. response 의 `Retry-After` 헤더 존중 (Phase 3 후 정식 도입 예정) ✅ **annotations 기반 게이팅** — destructive 도구는 user confirmation 후만 호출 ✅ **idempotent 도구는 retry 안전** — 네트워크 오류 시 재시도해도 안전 ## 다른 진영 - [Claude Code](/agents/claude-code) — MCP HTTP 직접 - [Claude Desktop](/agents/claude-desktop) — stdio - [OpenAI Custom GPT](/agents/openai-custom-gpt) — Actions --- # OpenAI Custom GPT — OpenAPI 3.1 Actions ainote 는 MCP tool 정의를 **OpenAPI 3.1 spec 으로 mirror** 한다. ChatGPT Plus / Team / Enterprise 의 Custom GPT Actions 가 이걸 그대로 import 한다 — hand-written translation 0줄. ## 5분 설정 ### 1. Custom GPT 생성 ChatGPT → 좌상단 `⊕ New chat` 옆 메뉴 → `Explore GPTs` → `+ Create` 또는 직접: ### 2. Configure 탭 → Actions 섹션 → "Create new action" ### 3. Schema → "Import from URL" URL 입력: ``` https://api.ainote.dev/api/mcp/openapi.json ``` ChatGPT 가 spec 을 파싱하고 50+개 도구를 자동으로 인식한다. `paths` 마다 하나의 action operation 으로 등록됨. ### 4. Authentication 설정 - **Auth Type**: `API Key` - **Auth Type** (sub): `Custom` - **Custom Header Name**: `Authorization` - **API Key value**: `McpKey ` ⚠️ `McpKey ` 접두사 + 공백 포함. `Bearer` 아님. ### 5. Privacy Policy URL ``` https://docs.ainote.dev/legal/privacy ``` ::: tip Verified Domain 공개 GPT Store 등록 시 OpenAI 는 `api.ainote.dev` hostname 의 root domain (`ainote.dev`) 인증 요구. 좌측 `Settings` → `Verify your domain` → DNS TXT 인증 진행. 비공개 / 개인 사용은 인증 불필요. ::: ### 6. GPT 기본 정보 - **Name**: `ainote — Multi-device Notes & Handoffs` - **Description**: `Agent-native notes & tasks with multi-device sync. 26 MCP-grade tools.` - **Instructions**: ``` You are an assistant connected to the user's ainote account. Use the ainote Actions for tasks, dev-docs, handoffs, vault sync, and file management. Each action's annotations (x-mcp-annotations) tell you whether the operation is read-only, destructive, idempotent, or open-world — gate destructive calls with explicit user confirmation. ``` - **Conversation starters**: - "Save a handoff for the work I just finished" - "What tasks are due today?" - "Pull my dev-docs to this machine" - "List my recent handoffs" ### 7. Save → Publish - **Only me**: 즉시 사용 가능 (verified domain 불필요) - **Anyone with a link**: 링크 공유. verified domain 권장 - **Public (GPT Store)**: verified domain 필수 ## 사용 시 주의 — 공유 시 키 정책 Custom GPT 는 **사용자별 자체 인증** 모드를 지원한다. 다른 사용자가 너의 GPT 를 쓰려면 본인 ainote MCP key 입력 필요. Auth 설정 시 `User-provided API key` 옵션을 선택하면 사용자가 처음 호출 시 키를 직접 입력하는 UI 가 나온다. ## Tool annotations 활용 OpenAPI mirror 는 MCP annotations 를 **`x-mcp-annotations`** extension 으로 노출: ```json { "paths": { "/api/mcp/tools/delete_task": { "post": { "x-mcp-annotations": { "readOnlyHint": false, "destructiveHint": true, "idempotentHint": true, "openWorldHint": false } } } } } ``` ChatGPT 가 이 extension 을 직접 읽지는 않지만 (현재 OpenAI spec 기준), Instructions 에서 ChatGPT 에게 "destructive 도구는 사용자 동의 후 호출" 라고 명시하면 GPT 가 spec 의 description 과 함께 판단 가능. ## 자주 묻는 문제 **"Import 시 'Could not parse OpenAPI'"** — `https://api.ainote.dev/api/mcp/openapi.json` 가 200 OK 인지 먼저 확인. cold start 시 timeout 가능 — 한 번 새로고침. **"401 Unauthorized"** — Authentication 의 `McpKey ` 접두사 + 공백 확인. 또는 key 만료 / revoke 됐는지 [app.ainote.dev](https://app.ainote.dev) → Settings → MCP keys 확인. **"Action returned empty result"** — 실제 도구 호출 결과는 `200` 응답 본문. ChatGPT 가 응답을 어떻게 인용할지는 spec 의 response schema 와 GPT instructions 에 달림. ## OpenAI 외 다른 OpenAPI 진영 같은 spec 으로 동작: - [LangChain / LangGraph](/agents/langchain) - AutoGen - CrewAI - 기타 OpenAPI 3.1 consumer → [전체 도구 매트릭스](/reference/annotations) --- # Windsurf — MCP (stdio) Windsurf 는 stdio MCP transport 만 지원 (2026-05 현재). HTTP transport 는 향후 추가 예정. ## 설정 `~/.codeium/windsurf/mcp_config.json`: ```json { "mcpServers": { "ainote": { "command": "npx", "args": ["-y", "@ainote/mcp"], "env": { "AINOTE_API_URL": "https://api.ainote.dev", "AINOTE_API_KEY": "" } } } } ``` ## 동작 확인 Windsurf 재시작 후 Cascade chat 에: > "내 ainote 태스크 추가해줘 — 내일 회의 준비" `create_task` 가 호출되면 정상. → [MCP key 발급](/agents/claude-code#mcp-key-발급) · [전체 도구 매트릭스](/reference/annotations) --- # 빌더 인증 ## 표면별 자격증명 규칙 | 표면 | 받는 자격증명 | SDK 동작 | |------|--------------|----------| | **REST** (`ai.tasks`, `ai.categories`, `ai.papers`) | Bearer 전용 (McpKey는 REST에서 401) | `Authorization: Bearer ` | | **MCP** (`ai.mcp`, `ai.vault`, `ai.sync`) | McpKey **또는** Bearer | McpKey 우선 → 없으면 Bearer 폴백 | ```ts new AiNote({ apiKey: '' }); // 같은 키를 양쪽에 (Bearer+McpKey) new AiNote({ auth: { type: 'bearer', token } }); // REST + MCP 둘 다 Bearer new AiNote({ auth: { type: 'mcpKey', key } }); // MCP 전용 ``` ## 방법 A — BYO 키 (지금 바로, ✅ 오늘 동작) 사용자가 ainote 계정에서 키를 발급 → 당신 앱(서버)에 연결 → SDK가 그 사용자 데이터를 읽고 씀. **키 발급 경로 (둘 중 하나):** 1. **무인증 onboarding 도구** — 계정 생성과 동시에 키를 받습니다. 서버 변경 0: ```bash # 신규 계정 curl -s https://api.ainote.dev/api/mcp -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{ "name":"signup_and_get_key", "arguments":{"email":"you@example.com","password":"min6chars","name":"My App"}}}' # 기존 계정 curl -s https://api.ainote.dev/api/mcp -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{ "name":"login_and_get_key", "arguments":{"email":"you@example.com","password":"..."}}}' ``` 응답 텍스트에 MCP 키가 들어 있습니다. (`signup`은 `email`+`password` 필수, `name` 선택 / `login`은 `email`+`password`.) 2. **앱/웹 설정** — 이미 ainote 사용자라면 ainote 앱 **설정 › MCP 키**에서 생성. 특징: - 서버 변경 0, 오늘 동작. - ⚠️ MCP 키는 **계정 전체 접근**(coarse) — 앱별/읽기전용 스코프 없음. 비번급 권한이라 **서버에 보관**. - 키는 그 사용자 데이터만 건드림(격리 보장). ## 방법 B — Sign in with logi (스케일 / 매끄러움) ::: warning 🚧 계획됨 — 아직 미제공 아래는 **향후 방향**입니다. 현재 SSO/OAuth 발급 흐름(issuer URL · 클라이언트 등록 · authorize/token 엔드포인트 · 스코프)은 공개돼 있지 않습니다. **지금 구현 가능한 건 방법 A뿐**입니다. 다중 사용자 스코프 권한이 필요하면 로드맵을 기다리거나 문의하세요. ::: logi(1pass)는 ainote의 IdP입니다. "Sign in with logi" 한 번으로 (계획): 1. 서드파티 앱 로그인 + **ainote 계정 자동 생성(JIT 프로비저닝)** + **자동 링크** 2. 검증된 동일 이메일이면 기존 ainote 계정과 자동 링크 (미검증 이메일/소셜 계정은 보호상 링크 안 함) 3. 앱은 ainote JWT(또는 logi `agent:read`/`agent:write` 토큰)를 받아 SDK에 Bearer로 전달 목표 장점: **이중가입 없음, raw 키 붙여넣기 없음, 스코프 있는 권한**. > 설계상 순서: MCP 커넥터(logi 토큰 → `/api/mcp`)는 **fail-closed** — SSO로 먼저 링크된 사용자만 통과(미링크 sub는 401, orphan 계정 안 만듦). 즉 "Sign in with logi 먼저 → 그다음 agent 토큰으로 MCP 호출". ## 단계 요약 | | 방법 A (BYO 키) | 방법 B (Sign in with logi) | |---|---|---| | 상태 | ✅ 오늘 동작 | 🚧 계획됨 | | 권한 | coarse (계정 전체) | scoped (`agent:read`/`agent:write`) | | 마찰 | 키 복사·붙여넣기 | 동의 1번 | | 빌드 | 0 (오늘) | (출시 후) logi 토큰 → SDK에 Bearer 전달 | 오늘 빌드/검증/데모는 **방법 A**로 합니다. ## WAF 403 {#waf-403} `api.ainote.dev` 앞단 WAF는 일부 비브라우저 User-Agent를 403으로 막습니다. SDK는 기본 `User-Agent: @ainote/sdk/`를 보내며 이는 **허용됩니다**(검증됨). 직접 curl로 호출하거나 `userAgent` 옵션을 커스텀했을 때 403이 나면 UA를 의심하세요 — 브라우저류 UA 또는 SDK 기본값을 사용하면 통과합니다. --- # ainote 위에 빌드하기 (개발자/SDK) 이 섹션은 **ainote를 직접 쓰는 사용자**가 아니라, **ainote를 백엔드처럼 자기 앱에 통합하는 개발자**를 위한 곳입니다. > 이용자용 문서(앱·에이전트 연결, 기능 설명)는 상단 다른 메뉴를 보세요. 여기는 "ainote 위에 짓는" 트랙입니다. ## 두 종류의 빌더 - **앱 개발자** — ainote를 자기 앱의 태스크·노트 백엔드로 사용. 타입 있는 REST(`ai.tasks` / `ai.categories` / `ai.papers`). - **동기화 도구 빌더** — vault·파일 동기화 프리미티브를 사용. MCP 미러(`ai.vault` / `ai.sync` / `ai.mcp`). ## 왜 ainote인가 이미 동작하는 백엔드를 공짜로 얻습니다 — 멀티디바이스 동기화, 충돌 해결, MCP/AI 접근, 크로스플랫폼. iCloud(CloudKit)만 쓰던 앱이 못 주던 "내 데이터를 AI 에이전트·CLI에서 접근" 을 그대로 얹을 수 있어요. ## 핵심 사실 - **SDK는 npm에 공개됨** — [`@ainote/sdk`](https://www.npmjs.com/package/@ainote/sdk) `npm install @ainote/sdk` 한 줄로 시작. 런타임 의존성 0, ESM+CJS+타입. - **API는 이미 라이브** (`https://api.ainote.dev`) — 우리 Flutter·iOS·mac·watch 앱이 매일 쓰는 검증된 표면. - **두 표면**: 구조화된 JSON을 주는 **REST**(앱용)와, 텍스트를 주는 **MCP JSON-RPC**(에이전트용). - **하나의 키, 두 헤더**: 같은 ainote 키가 REST에선 `Bearer`, MCP에선 `McpKey`로 동작. ## 다음 단계 - [5분 Quickstart](/build/quickstart) — SDK 설치 + 첫 호출 - [인증](/build/auth) — 키 발급 / Sign in with logi - [@ainote/sdk 레퍼런스](/build/sdk) — 메서드 전체 - [동기화 백엔드로 쓰기](/build/sync-backend) — ainote를 sync 허브로 --- # 빌더 Quickstart (5분) ## 1. 설치 ```bash npm install @ainote/sdk ``` - 런타임 의존성 0 (Node ≥18의 내장 `fetch` 사용), ESM + CJS + 타입 제공. ## 2. 키 발급 키 하나면 됩니다. 두 가지 방법 — 둘 다 **오늘 동작**합니다. **A. 계정이 없다면 — 무인증 onboarding 도구로 즉시 발급** (`signup_and_get_key`): ```bash curl -s https://api.ainote.dev/api/mcp \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call", "params":{"name":"signup_and_get_key", "arguments":{"email":"you@example.com","password":"min6chars"}}}' # → 응답 텍스트에 새 계정 + MCP 키 포함. 이미 계정이 있으면 login_and_get_key 사용(email/password 동일). ``` **B. 이미 ainote 사용자라면 — 앱/웹 설정에서 발급**: ainote 앱 → **설정 › MCP 키**에서 키 생성. `apiKey`로 넘기면 SDK가 REST엔 `Bearer`, MCP엔 `McpKey` 헤더로 같은 키를 보냅니다(표면별 규칙은 [인증](/build/auth) 참고). > 키는 **서버에 보관**하세요. 계정 전체 접근 권한(비번급)이라 모바일/웹 클라이언트 번들에 넣지 마세요(유출 위험). **키가 동작하는지 1줄 확인** (REST Bearer, HTTP 200 기대): ```bash curl -s -o /dev/null -w '%{http_code}\n' \ https://api.ainote.dev/api/tasks?per_page=1 \ -H "Authorization: Bearer $AINOTE_KEY" # 200 이면 끝. 403 이면 → User-Agent/WAF 이슈 ([인증](/build/auth#waf-403) 참고) ``` 자세한 인증 옵션은 [인증](/build/auth) 참고. ## 3. 첫 호출 (REST — 타입 있음) ```ts import { AiNote } from '@ainote/sdk'; const ai = new AiNote({ apiKey: process.env.AINOTE_KEY! }); // 태스크 목록 (타입 Task[]) const { data: tasks } = await ai.tasks.list({ is_important: true }); // 생성 → 수정 → 삭제 const task = await ai.tasks.create({ content: '첫 태스크', due_date: '2026-06-10' }); await ai.tasks.update(task.id, { is_important: true }); await ai.tasks.delete(task.id); // 노트(papers) const { data: notes } = await ai.papers.list({ include_content: false }); ``` ## 4. 첫 호출 (MCP — vault/sync, 텍스트) ```ts const res = await ai.mcp.call('list_tasks', { limit: 1 }); console.log(res.text); // "Found 1 tasks: ⏳ ..." await ai.vault.list(); await ai.sync.pendingConflicts(); ``` ## 5. 에러 처리 ```ts import { ValidationError, RateLimitError, AuthError } from '@ainote/sdk'; // sleep() / reauth() 는 앱에서 제공하는 헬퍼 (SDK export 아님) try { await ai.tasks.create({ content: '' }); } catch (e) { if (e instanceof ValidationError) console.log(e.fieldErrors); else if (e instanceof RateLimitError) await sleep(e.retryAfterMs ?? 1000); else if (e instanceof AuthError) reauth(); } ``` 다음: [@ainote/sdk 레퍼런스](/build/sdk) · [동기화 백엔드로 쓰기](/build/sync-backend) --- # @ainote/sdk 레퍼런스 [![npm](https://img.shields.io/npm/v/@ainote/sdk.svg)](https://www.npmjs.com/package/@ainote/sdk) — npm에 공개됨. ```bash npm install @ainote/sdk ``` ## 클라이언트 ```ts import { AiNote } from '@ainote/sdk'; const ai = new AiNote({ apiKey, // 또는 auth: { type:'bearer'|'mcpKey', ... } baseUrl, // 기본 https://api.ainote.dev userAgent, // 기본 @ainote/sdk/ maxRetries, // 기본 2 — 멱등(GET/HEAD/DELETE) 요청만 429/5xx 지수 백오프 재시도. 쓰기·MCP는 재시도 안 함 fetchImpl, // 커스텀 fetch 주입 (테스트/프록시) }); ``` ## REST 리소스 (타입 있음, Bearer) ### `ai.tasks` | 메서드 | HTTP | 반환 | |--------|------|------| | `list(query?)` | GET /api/tasks | `{ data: Task[], meta? }` | | `listAll(query?)` | (페이지 순회) | `AsyncGenerator` | | `get(id)` | GET /api/tasks/:id | `Task` | | `create(input)` | POST /api/tasks | `Task` | | `update(id, input)` | PATCH /api/tasks/:id | `Task` | | `delete(id)` | DELETE /api/tasks/:id | `void` | 필터: `category_id, completed, is_important, kanban_status, due_date_from, due_date_to, sort_by, sort_order, page, per_page`. ### `ai.categories` `list()` / `get(id)` / `create(input)` / `update(id, input)` / `delete(id)`. ### `ai.papers` `list(query?)` / `get(id)` / `create(input)` / `update(id, input)` / `delete(id)` / `publish(id, published)`. > `Paper` 타입은 실제 `paper_json` 응답에 맞춰져 있습니다(folder/`_count`/source_url/is_link_note/ai_*). 생성 OpenAPI 스키마(DB 모델)와는 다름. ## MCP 미러 (McpKey 또는 Bearer) REST에 없는 vault/파일동기화/핸드오프/dev-docs용. 응답은 정직하게 세 형태로 노출됩니다 — `{ content, text, resources }`. ```ts const { content, text, resources } = await ai.mcp.call('list_tasks', { limit: 1 }); // text → 사람이 읽는 문자열 (모든 text-type content 합침) // resources → 구조화 JSON 배열 (resource-type content 의 JSON payload 파싱; 없으면 []) // content → 원본 content 배열 (가공 전) ``` - `ai.vault.{list, create, clone, sync, connectStatus}` - `ai.sync.{push, pull, list, diff, merge, pendingConflicts}` > 프로그램으로 동기화를 다룰 땐 `text`(사람용)가 아니라 **`resources`**(기계용 JSON)를 파싱하세요. ## 에러 클래스 `AiNoteError` ← `AiNoteApiError` ← `AuthError`(401/403) · `NotFoundError`(404) · `ValidationError`(422, `.fieldErrors`) · `RateLimitError`(429, `.retryAfterMs`). > **MCP 에러 주의:** `ai.mcp`/`ai.vault`/`ai.sync` 도구 실패는 JSON-RPC 에러가 HTTP **200** 본문에 담겨 옵니다. SDK는 `AiNoteApiError`를 던지되 `status===200`, `code`는 JSON-RPC 에러코드입니다 — `e.status>=400`이 아니라 **에러 클래스**로 분기하세요. ```ts import { ValidationError } from '@ainote/sdk'; try { await ai.tasks.create({ content: '' }); } catch (e) { if (e instanceof ValidationError) console.log(e.fieldErrors); } ``` ## v0.1 범위 - REST: tasks, categories, papers - MCP 미러: vault, sync, 임의 `mcp.call` - 미포함: 로그인 헬퍼, 알림/캘린더/텔레그램 등 (필요 시 추가) --- # ainote를 동기화 백엔드로 쓰기 핵심 아이디어: **사용자가 ainote 키 하나만 있으면, 다른 앱이 그 키로 그 사용자의 데이터를 읽고 쓰며, ainote가 모든 클라이언트 간에 동기화합니다.** 당신 앱은 동기화 엔진을 만들 필요가 없어요. ## 두 종류의 "동기화" ainote에는 별개의 두 동기화가 있습니다. 목적에 맞는 걸 쓰세요. ### 1. 태스크/노트 DB 동기화 (구조화된 데이터) 할 일·노트를 ainote에 쓰면, 그 사용자의 **ainote 앱 · Claude · CLI · 당신 앱 · 또 다른 앱** 전부에서 같은 데이터가 보입니다. ainote가 공유 백엔드이기 때문이에요. ```ts // 당신 앱에서 쓰기 await ai.tasks.create({ content: '회의 준비', due_date: '2026-06-12' }); // → 사용자의 모든 ainote 표면에 즉시 나타남 ``` 타입 있는 REST(`ai.tasks` / `ai.papers` / `ai.categories`)로 접근. 인증은 `Bearer`. ### 2. Vault 파일 동기화 (마크다운/텍스트) CLAUDE.md·메모 파일 등 **파일**을 git-backed vault로 동기화. CAS·3-way merge·충돌 추적 포함. MCP 미러로 접근 — 응답은 사람이 읽는 `text`와 **구조화 JSON `resources`** 둘 다 옵니다. ```ts // push: 보호 경로는 CAS를 위해 base_sha 필요 (멀티디바이스 silent overwrite 방지). // 권장 흐름: 먼저 sync_list/diff 로 remote sha 확인 → base_sha 와 함께 push. await ai.sync.push({ path: 'notes/idea.md', content: '...', base_sha: '' }); const conflicts = await ai.sync.pendingConflicts(); console.log(conflicts.text); // "✅ no pending conflicts" (사람용) const [data] = conflicts.resources; // { vault_id, count, conflicts: [...] } (기계용 JSON) const merged = await ai.sync.diff({ path: 'notes/idea.md' }); ``` > 동기화 엔진을 직접 짠다면 `.text`(사람용)가 아니라 **`.resources`(구조화 JSON)** 를 파싱하세요. 충돌 머지 사이클(`sync_merge` → `base_sha`로 `sync_push`)과 보호 경로의 base_sha 규칙은 [sync 레퍼런스](/reference/sync-push)를 참고하세요. ## 키 하나로 가능한가? — 네 MCP 키 하나가 그 사용자의 모든 MCP 도구(태스크·노트·vault·파일동기화)를 엽니다. 그래서 외부 앱이 키만 받으면 동기화를 바로 쓸 수 있습니다. 단: - MCP 키는 **계정 전체 접근**(coarse). 서버에 보관하세요. - `apiKey`로 넘기면 SDK가 REST엔 `Bearer`, MCP엔 `McpKey`로 같은 키를 보냅니다. `auth: { type: 'mcpKey' }`로 고정하면 MCP 전용 — 그땐 `ai.mcp` / `ai.sync` / `ai.vault`만. 확실한 REST 접근은 Bearer 토큰(ainote JWT) 권장. - 다중 사용자 서비스: 오늘은 사용자별 BYO 키(또는 onboarding 도구 `signup_and_get_key`)로 구현합니다. 스코프 있는 권한의 [Sign in with logi](/build/auth#방법-b-sign-in-with-logi-스케일-매끄러움)는 **🚧 계획됨 — 아직 미제공**. ## 전형적 통합 흐름 1. 사용자가 ainote 계정 + 키 확보 (Sign in with logi는 🚧 계획됨) 2. 당신 앱(서버)이 키를 안전 보관 3. `@ainote/sdk`로 그 사용자의 데이터 read/write 4. ainote가 모든 디바이스·앱·에이전트 간 동기화 — 끝 자세한 메서드: [@ainote/sdk 레퍼런스](/build/sdk). --- # Cert Mirror — API 레퍼런스 base URL: `https://api.ainote.dev` 전체 개요는 [overview](/cert-mirror/overview) 참조. ## 인증 모든 엔드포인트는 `Authorization: McpKey ` 헤더 필수. `` 은 본인 ainote 계정의 `UserMirrorCredential#token` (64 hex chars, deterministic encrypted). ### 발급 — 계정당 1개 본인 ainote 계정의 primary API key (`UserMcpKey` 또는 `User.api_key`) 로 발급 endpoint 를 호출합니다. 결과 `token` + `hmac_secret` 을 클라이언트 쪽 secret store (macOS Keychain, 1Password, OS env 등) 에 보관. #### 권장: CLI 스크립트 ```bash ainote-cert-mirror issue ``` 자세한 셋업은 [setup-guide](/cert-mirror/setup-guide) + [cli](/cert-mirror/cli). #### Raw HTTP (Linux/CI 환경) ```bash curl -fsSL -X POST https://api.ainote.dev/api/cert_mirror/credentials \ -H "User-Agent: ainote-cli/1.0" \ -H "Authorization: McpKey " \ -H "Content-Type: application/json" \ -d '{}' ``` **응답** (신규 발급): ```json { "status": "created", "token": "<64 hex>", "hmac_secret": "<64 hex>", "message": "Save hmac_secret now — it cannot be retrieved later." } ``` **응답** (이미 존재, rotate 안 함): ```json { "status": "existing", "token": "<64 hex — 동일 값>", "hmac_secret": null, "message": "Credential already exists. Pass rotate=true to issue new secrets ..." } ``` `rotate=true` 시 destroy + 재생성 — **기존 blob 들 HMAC 검증 실패 → 재업로드 필요**. #### Raw HTTP — 폐기 ```bash curl -fsSL -X DELETE https://api.ainote.dev/api/cert_mirror/credentials \ -H "User-Agent: ainote-cli/1.0" \ -H "Authorization: McpKey " ``` ### 두 값의 차이 | 필드 | 암호화 | 재조회 | 용도 | |------|--------|--------|------| | `token` | deterministic | ✅ 동일 값 반환 | `Authorization: McpKey ` 인증 | | `hmac_secret` | non-deterministic | ❌ 발급 시점에만 평문 노출 | 업로드 시 body HMAC-SHA256 계산. **반드시 별도 보관** | `hmac_secret` 회전 시 모든 기존 blob 의 signature 검증이 깨지므로 전체 cert mirror 재업로드 필요. ## User-Agent (필수) Render edge WAF 가 default `curl/*` 또는 `Ruby` UA 를 차단합니다. 모든 요청에 명시: ``` User-Agent: ainote-fastlane-mirror/1.0 ``` (이름 자유 — `/` 형식이면 됨) 빠지면 응답: ```json HTTP/1.1 403 {"error":"Forbidden. Your request has been blocked."} ``` ## `POST /api/cert_mirror` 새 cert tarball 업로드. ### 요청 ```http POST /api/cert_mirror HTTP/1.1 Host: api.ainote.dev Authorization: McpKey User-Agent: ainote-fastlane-mirror/1.0 Content-Type: application/octet-stream Content-Length: X-Commit-Sha: <40 hex chars — apple-certs git HEAD> X-Tarball-Sha256: <64 hex chars — body 의 SHA-256> X-Mirror-Signature: sha256=<64 hex chars — HMAC-SHA256(hmac_secret, body)> X-Uploaded-By: <자유 라벨, 예: "fastlane:seunghan"> ``` ### 헤더 표 | 헤더 | 필수 | 설명 | |------|------|------| | `Authorization` | ✅ | `McpKey ` | | `User-Agent` | ✅ | WAF 회피 (위 참조) | | `Content-Type` | ✅ | `application/octet-stream` (멀티파트 금지 — HMAC 비트 정확 매칭) | | `Content-Length` | ✅ | body 바이트 수. 50 × 1024 × 1024 초과 시 즉시 413 | | `X-Commit-Sha` | ✅ | apple-certs repo 의 git HEAD SHA. `\A[0-9a-f]{40}\z` | | `X-Tarball-Sha256` | ✅ | body 의 SHA-256 hex (서버가 재계산 후 비교) | | `X-Mirror-Signature` | ✅ | `sha256=` (서버가 재계산 후 timing-safe 비교) | | `X-Uploaded-By` | | 추적용 자유 라벨 (e.g. `fastlane:`) | ### 응답 (200) ```json { "commit_sha": "268112ce4ed3dec5e04cdf7e517cb72e04772b45", "sha256": "53c9b81ee0816f70c37f8ea9b790a5f8d4e6c7795f119f24820541de84bf7319", "byte_size": 60781, "uploaded_at": "2026-05-16T23:16:43.855Z" } ``` ### 에러 | HTTP | 의미 | 해결 | |------|------|------| | 401 | token 없음 / 잘못됨 | `UserMirrorCredential` 재발급 | | 403 | WAF 차단 (default UA) | `User-Agent` 명시 | | 411 | `Content-Length` 헤더 누락 | 헤더 추가 | | 413 | body 가 50 MB 초과 | apple-certs repo 정리 / 분할 (현재 미지원) | | 422 | `X-Commit-Sha` 가 40 hex 형식 아님 | SHA 검증 | | 422 | `X-Tarball-Sha256` 가 body 와 불일치 | 전송 중 손상, 재시도 | | 422 | `X-Mirror-Signature` HMAC 검증 실패 | `hmac_secret` 잘못 / body 변조 | ## `GET /api/cert_mirror` 내 모든 blob 목록 (최신 순). 메타데이터만 반환 — 본문 다운로드 X. ### 요청 ```http GET /api/cert_mirror HTTP/1.1 Authorization: McpKey User-Agent: ainote-cli/1.0 ``` ### 응답 (200) ```json { "blobs": [ { "commit_sha": "268112ce4ed3dec5e04cdf7e517cb72e04772b45", "sha256": "53c9b81e…", "byte_size": 60781, "uploaded_at": "2026-05-16T23:16:43.855Z", "uploaded_by": "fastlane:seunghan" } ] } ``` 빈 상태: ```json { "blobs": [] } ``` ## `GET /api/cert_mirror/latest` 가장 최근 업로드된 blob 의 메타데이터. 폴링 / health check 용. ### 응답 (200) ```json { "commit_sha": "268112ce…", "sha256": "53c9b81e…", "byte_size": 60781, "uploaded_at": "2026-05-16T23:16:43.855Z", "download_url": "/api/cert_mirror/268112ce4ed3dec5e04cdf7e517cb72e04772b45/download" } ``` blob 없으면 404: ```json { "error": "no blobs yet" } ``` ## `GET /api/cert_mirror/:sha/download` 특정 commit_sha 의 tarball 본문 다운로드. ### 요청 ```bash curl -fsSL \ -H "User-Agent: ainote-cli/1.0" \ -H "Authorization: McpKey $TOKEN" \ -o apple-certs.tgz \ https://api.ainote.dev/api/cert_mirror/268112ce4ed3dec5e04cdf7e517cb72e04772b45/download ``` ### 응답 `Content-Type: application/octet-stream`, body = tarball 바이너리. `Content-Length` + `X-Tarball-Sha256` 헤더 함께 반환 (클라이언트에서 다운로드 후 재검증 권장). ## 다음 - [Overview & 사용처](/cert-mirror/overview) - [fastlane 통합](/cert-mirror/fastlane) --- # ainote-cert-mirror — CLI 레퍼런스 macOS 용 zsh 스크립트. cert mirror credential 관리 + endpoint 호출 + Keychain 통합을 한 줄로 묶음. 처음 셋업하는 경우 [setup-guide](/cert-mirror/setup-guide) 부터. ## 설치 스크립트 본문은 [GitHub](https://github.com/seunghan91/ainote/blob/main/scripts/ainote-cert-mirror.sh) 또는 본인 mac 의 `~/scripts/ainote-cert-mirror.sh`. PATH 에 `~/scripts` 추가 권장. ## 의존성 - zsh (macOS 기본) - `curl` (macOS 기본) - `python3` (macOS 기본 — JSON 파싱) - `security` (macOS 기본 — Keychain) - `$AINOTE_API_KEY` env — 본인 ainote primary UserMcpKey 또는 User.api_key 선택: - `$AINOTE_API_BASE` env — 기본 `https://api.ainote.dev`. 자기 호스트 ainote_server 운영 중이면 override ## ⚠️ 계정 매칭 주의 `AINOTE_API_KEY` 환경변수가 가리키는 ainote 계정 = `UserMirrorCredential` 이 만들어질 계정입니다. 여러 계정 (예: 메일별 분리) 보유 시 잘못된 키로 `issue` 하면 **다른 계정에 새 credential 이 만들어집니다** — 기존 계정의 cert mirror 와 무관한 빈 mirror 가 생성되는 함정. 조치: - `ainote-cert-mirror show` 로 env 의 키 길이만 우선 확인 - 미심쩍으면 `issue` 전에 `list` 호출 — token 이 다른 계정 것이면 `unauthorized` 또는 빈 blob 응답 - 실수로 만들었으면 `revoke` 로 즉시 정리 (blob 0개 상태면 부수효과 없음) ## Subcommands ### `issue [--rotate]` UserMirrorCredential 발급 / 갱신. 결과 token + hmac_secret 을 Keychain 에 저장. | 옵션 | 동작 | |---|---| | (없음) | 기존 credential 있으면 token 만 Keychain 동기화 (hmac 은 못 받음). 없으면 신규 발급 | | `--rotate` | 기존 destroy + 신규 생성. token + hmac 모두 새로 받음. ⚠️ 기존 blob 들은 HMAC 검증 실패 | 서버 호출: `POST /api/cert_mirror/credentials` body `{"rotate": true/false}` ```bash # 첫 발급 ainote-cert-mirror issue # hmac_secret 분실 시 ainote-cert-mirror issue --rotate ``` ### `list` 본인 blob 목록 (최신순, 최대 20). JSON 출력. 서버 호출: `GET /api/cert_mirror` ```bash ainote-cert-mirror list ``` ```json { "blobs": [ { "commit_sha": "268112ce…", "sha256": "53c9b81e…", "byte_size": 60781, "uploaded_at": "2026-05-16T23:16:43.855Z", "uploaded_by": "fastlane:seunghan" } ] } ``` ### `latest` 가장 최근 blob 1개 메타데이터. heartbeat / CI 검증용. ```bash ainote-cert-mirror latest ``` ```json { "commit_sha": "268112ce…", "byte_size": 60781, "uploaded_at": "2026-05-16T23:16:43.855Z" } ``` 빈 상태: ```json { "blobs": [] } ``` ### `verify` 엔드포인트 200 + Keychain entry 존재 + blob 카운트 확인. 헬스체크용. ```bash ainote-cert-mirror verify ``` ``` ✅ Endpoint https://api.ainote.dev/api/cert_mirror reachable (200) ✅ Keychain: token (64 chars) + hmac (64 chars) present ✅ Blobs uploaded: 1 ``` 실패 시 exit code 1 — CI 에서 그대로 활용 가능. ### `show` Keychain entry 길이 + env 상태만 표시. **값은 노출 X**. 디버깅용. ```bash ainote-cert-mirror show ``` ``` ℹ️ token: 64 chars (service: "ainote mirror token") ℹ️ hmac: 64 chars (service: "ainote mirror hmac") ℹ️ API base: https://api.ainote.dev ℹ️ AINOTE_API_KEY env: set (64 chars) ``` ### `revoke` 서버 credential destroy + Keychain entry 2개 삭제. 확인 prompt. 서버 호출: `DELETE /api/cert_mirror/credentials` ```bash ainote-cert-mirror revoke # ⚠️ Destroy your UserMirrorCredential? Previously uploaded blobs become unreachable. [y/N] y ``` 기존 blob 들은 서버 디스크에 남지만 새 credential 발급 + 재업로드 전까지 다운로드 불가 (HMAC secret 분실). ### `help` (또는 `-h`, `--help`) usage 출력. ## Keychain entry 이름 스크립트가 사용하는 service: - `ainote mirror token` — UserMirrorCredential.token (64 hex) - `ainote mirror hmac` — UserMirrorCredential.hmac_secret (64 hex) 수동 조회: ```bash security find-generic-password -s "ainote mirror token" -w security find-generic-password -s "ainote mirror hmac" -w ``` 수동 등록 (다른 mac 에서 복사 시): ```bash security add-generic-password -s "ainote mirror token" -a "$USER" -w '' ``` ## Linux / CI 환경 스크립트는 macOS Keychain 의존. Linux/Docker/GitHub Actions 등은 [api 페이지](/cert-mirror/api) 의 curl 패턴 + 환경변수 (`AINOTE_MIRROR_TOKEN`, `AINOTE_MIRROR_HMAC_SECRET`) 직접 사용 권장. ## 함정 | 증상 | 원인 | 해결 | |---|---|---| | `AINOTE_API_KEY ... not set` | env 누락 (혹은 `subshell` 에서 안 보임) | `~/.zshrc` export 후 `source ~/.zshrc` | | `Unexpected server response` | 서버가 새 status 코드 반환 | 서버 배포 확인, 필요 시 스크립트 업데이트 | | `issue` 가 `existing` 반환했는데 Keychain hmac 없음 | 이전 mac 에서 발급 → 현재 mac 으로 hmac 미동기화 | iCloud Keychain 확인 후 안 되면 `issue --rotate` | | `verify` 의 endpoint HTTP 401 | UserMirrorCredential 이 삭제됨 (다른 mac 에서 revoke?) | `issue` 다시 (기존 token 회수) 또는 `--rotate` | | 스크립트 자체 zsh 문법 오류 | bash 로 실행 시도 | shebang `#!/bin/zsh` 명시 + `zsh ainote-cert-mirror.sh` | ## 환경변수 정리 | 변수 | 필수 | 기본 | 용도 | |---|---|---|---| | `AINOTE_API_KEY` | ✅ (issue/revoke 시) | — | UserMcpKey, primary auth | | `AINOTE_API_BASE` | | `https://api.ainote.dev` | self-host 시 override | (`AINOTE_MIRROR_TOKEN`, `AINOTE_MIRROR_HMAC_SECRET` 같은 mirror-side env 는 fastlane 워크플로우에서 따로 export — 스크립트는 Keychain 우선) ## 다음 - [5분 셋업 가이드](/cert-mirror/setup-guide) - [fastlane 통합](/cert-mirror/fastlane) - [HTTP API 레퍼런스](/cert-mirror/api) --- # Cert Mirror — fastlane 통합 `fastlane match` 워크플로우 끝에 ainote cert_mirror 업로드를 붙이는 drop-in 패턴. 검증된 production 코드 (ainote 의 ios_native). 전체 개요는 [overview](/cert-mirror/overview), API 시그니처는 [api](/cert-mirror/api). ## 사전 준비 ### 1. UserMirrorCredential 발급 (본인 ainote 계정) 본인 계정에서 1개만 발급받으면 됩니다. 자세한 절차는 [api 인증 섹션](/cert-mirror/api#인증). 발급 결과로 받는 두 값: - `token` — 64 hex, `Authorization: McpKey ` 헤더 - `hmac_secret` — 64 hex, 업로드 body HMAC-SHA256 계산용 (발급 시점에만 평문 확인 가능, **별도 보관 필수**) ### 2. macOS Keychain 에 secret 저장 ```bash security add-generic-password -s " mirror token" -a "$USER" -w '' security add-generic-password -s " mirror hmac" -a "$USER" -w '' # MATCH_PASSWORD 도 같은 패턴 security add-generic-password -s " match" -a "$USER" -w '' ``` iCloud Keychain 활성화돼있으면 맥미니 ↔ 맥북 자동 동기화. ## Fastfile drop-in `fastlane/Fastfile`: ```ruby default_platform(:ios) MIRROR_URL = ENV["AINOTE_MIRROR_URL"] || "https://api.ainote.dev/api/cert_mirror" MIRROR_TOKEN = ENV["AINOTE_MIRROR_TOKEN"] MIRROR_HMAC = ENV["AINOTE_MIRROR_HMAC_SECRET"] MATCH_REPO = ENV["MATCH_REPO_PATH"] || File.expand_path("~/apple-certs") platform :ios do desc "Sync code signing + mirror to ainote" lane :sync_signing do key = app_store_connect_api_key( key_id: ENV["ASC_KEY_ID"], issuer_id: ENV["ASC_ISSUER_ID"], key_filepath: ENV["ASC_API_KEY_PATH"], duration: 1200, in_house: false, ) match(type: "development", readonly: false, api_key: key) match(type: "appstore", readonly: false, api_key: key) mirror_to_ainote end # ────────── private helpers ────────── private_lane :mirror_to_ainote do if [MIRROR_URL, MIRROR_TOKEN, MIRROR_HMAC].any?(&:nil?) UI.important("⚠ mirror env missing — skipping") next end unless Dir.exist?(MATCH_REPO) UI.important("⚠ MATCH_REPO_PATH not found: #{MATCH_REPO} — skipping") next end require "digest" require "openssl" require "net/http" tarball = nil begin Dir.chdir(MATCH_REPO) do commit_sha = sh("git rev-parse HEAD").strip unless commit_sha.match?(/\A[0-9a-f]{40}\z/) UI.user_error!("invalid HEAD sha: #{commit_sha.inspect}") end tarball = "/tmp/match-mirror-#{commit_sha[0..7]}.tgz" # git archive: bsd vs gnu tar portability sh("git archive --format=tar.gz -o #{tarball} HEAD") tarball_size = File.size(tarball) # ── streaming HMAC + SHA256 (메모리에 통째로 안 올림) ── hmac = OpenSSL::HMAC.new(MIRROR_HMAC, OpenSSL::Digest::SHA256.new) sha = OpenSSL::Digest::SHA256.new File.open(tarball, "rb") do |f| while (chunk = f.read(64 * 1024)) hmac.update(chunk) sha.update(chunk) end end hmac_hex = hmac.hexdigest sha256_hex = sha.hexdigest # ── HTTP POST (body_stream — 업로드도 메모리 스트림 X) ── uri = URI.parse(MIRROR_URL) Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 60) do |http| req = Net::HTTP::Post.new(uri) req["Authorization"] = "McpKey #{MIRROR_TOKEN}" req["Content-Type"] = "application/octet-stream" req["Content-Length"] = tarball_size.to_s req["X-Commit-Sha"] = commit_sha req["X-Tarball-Sha256"] = sha256_hex req["X-Mirror-Signature"] = "sha256=#{hmac_hex}" req["X-Uploaded-By"] = "fastlane:#{ENV['USER'] || 'unknown'}" req["User-Agent"] = "ainote-fastlane-mirror/1.0" # WAF 우회 필수 File.open(tarball, "rb") do |upload| req.body_stream = upload resp = http.request(req) if resp.code.to_i.between?(200, 299) UI.success("✓ mirrored sha=#{commit_sha[0..7]} bytes=#{tarball_size}") else UI.important("⚠ mirror HTTP #{resp.code}: #{resp.body.to_s[0..200]}") end end end end rescue => e UI.important("⚠ mirror failed: #{e.class}: #{e.message} — git push primary OK, retry next sync") ensure File.delete(tarball) if tarball && File.exist?(tarball) end end end ``` 핵심 디자인: - mirror 실패해도 lane 자체는 안 깨짐 (primary git push 는 이미 성공) - `body_stream` + chunk loop 로 50MB tarball 도 메모리에 통째로 안 올림 - `git archive` 사용 — `tar --exclude='.git'` 는 bsd 와 gnu 사이 호환성 깨짐 ## 실행 ```bash cd export MATCH_PASSWORD="$(security find-generic-password -s 'your-app match' -w)" export AINOTE_MIRROR_TOKEN="$(security find-generic-password -s 'your-app mirror token' -w)" export AINOTE_MIRROR_HMAC_SECRET="$(security find-generic-password -s 'your-app mirror hmac' -w)" export ASC_KEY_ID="" export ASC_ISSUER_ID="" export ASC_API_KEY_PATH="$HOME/.appstoreconnect/private_keys/AuthKey_.p8" bundle exec fastlane sync_signing ``` 기대 로그: ``` [hh:mm:ss] $ git rev-parse HEAD [hh:mm:ss] 268112ce4ed3dec5e04cdf7e517cb72e04772b45 [hh:mm:ss] $ git archive --format=tar.gz -o /tmp/match-mirror-268112ce.tgz HEAD [hh:mm:ss] ✓ mirrored sha=268112ce bytes=60781 ``` ## 검증 ```bash TOKEN="$(security find-generic-password -s 'your-app mirror token' -w)" curl -sS -H "User-Agent: ainote-cli/1.0" \ -H "Authorization: McpKey $TOKEN" \ https://api.ainote.dev/api/cert_mirror | jq ``` ```json { "blobs": [ { "commit_sha": "268112ce…", "sha256": "53c9b81e…", "byte_size": 60781, "uploaded_at": "2026-05-16T23:16:43.855Z", "uploaded_by": "fastlane:seunghan" } ] } ``` ## 신규 mac / 재해 복구 primary (`apple-certs` GitHub repo) 가 살아있는 경우: ```bash gh repo clone /apple-certs ~/apple-certs cd ~/apple-certs && git fetch origin master && git checkout master # Keychain 에 MATCH_PASSWORD 복원 후 fastlane fetch_signing ``` GitHub repo 가 사라진 경우 — ainote mirror 에서 복원: ```bash TOKEN="$(security find-generic-password -s 'your-app mirror token' -w)" # 가장 최근 commit_sha 확인 SHA=$(curl -fsSL -H "User-Agent: ainote-cli/1.0" -H "Authorization: McpKey $TOKEN" \ https://api.ainote.dev/api/cert_mirror | jq -r '.blobs[0].commit_sha') # tarball 다운로드 curl -fsSL -H "User-Agent: ainote-cli/1.0" -H "Authorization: McpKey $TOKEN" \ -o /tmp/apple-certs.tgz \ "https://api.ainote.dev/api/cert_mirror/$SHA/download" # 새 git repo 로 풀어서 다시 push (GitHub 새 repo 생성 후) mkdir ~/apple-certs && cd ~/apple-certs tar -xzf /tmp/apple-certs.tgz git init -b master && git add -A && git commit -m "restore from ainote mirror sha=$SHA" git remote add origin git@github.com:/apple-certs.git git push -u origin master ``` ## 함정 | 증상 | 원인 | 해결 | |------|------|------| | HTTP 403 "Forbidden. Your request has been blocked" | Render WAF 가 default UA 차단 | `User-Agent` 헤더 명시 | | HTTP 422 "X-Mirror-Signature invalid" | `MIRROR_HMAC` 가 발급 시점 값과 다름 | Keychain 값 확인, 필요시 `UserMirrorCredential` 재발급 | | `fatal: ambiguous argument 'HEAD'` | apple-certs 로컬 clone 이 `main` branch (빈 것) | `git fetch origin master && git checkout master` | | `invalid number: '-----BEGIN'` (match 자체 에러) | match 의 `api_key_path:` 에 raw `.p8` 전달 — JSON 기대 | `api_key:` 에 `app_store_connect_api_key` action 의 hash 전달 | 전체 함정 목록은 [overview](/cert-mirror/overview#알려진-제약) 참조. ## 다음 - [API 레퍼런스](/cert-mirror/api) - [Overview](/cert-mirror/overview) --- # Cert Mirror — 무엇이고 왜 있나 ainote 계정에 **fastlane match 의 cert + provisioning profile 묶음을 암호화 백업으로 저장**할 수 있는 dev-facing API. iOS/macOS 빌드 자동화를 운영하는 사람이 GitHub 외에 두 번째 안전망을 원할 때 씁니다. ## 한 줄 요약 > fastlane match 가 git push 한 디렉터리를 통째로 tarball 로 묶어서 ainote 에 HMAC 검증 업로드 → 나중에 GitHub repo 가 사라져도 ainote 에서 다시 받을 수 있게. ## 누구를 위한 기능인가 ainote 계정만 있으면 누구나 — 특히 다음 사용자들이 효용을 봅니다: - **Claude Code / Cursor / Windsurf 같은 AI agent** — TestFlight 자동화 워크플로우를 운영하면서 자기가 만든 IPA 의 signing artefact 까지 안전하게 책임지고 싶을 때 - **fastlane match 사용자** — GitHub repo 1개에 의존하기 불안할 때 두 번째 안전망으로 - **모든 ainote 사용자** — cert tarball 외에도 "암호화된 작은 바이너리 백업" 이 필요한 모든 케이스에 일반적으로 활용 가능 (현재 1차 use case 는 cert mirror) ainote 의 노트 / 메모리 / 동기화 같은 end-user 기능과는 별도 layer 의 dev API 입니다. 계정만 있으면 동일 인증 체계 (`McpKey` 토큰) 안에서 추가 발급으로 바로 쓸 수 있습니다. ## 왜 만들었나 fastlane match 의 표준 저장 위치는 GitHub private repo. 이 자체가 git 이라 안전하지만: 1. **단일 의존성** — GitHub 가 다운되거나 organization 이 정지되면 빌드 자동화 전체가 멈춤 2. **계정 이전** — 회사·개인 GitHub 계정 옮길 때 cert repo migration 이 손이 많이 감 3. **AI agent 가 자기 빌드를 추적하기 어려움** — git 은 LLM 이 직접 들춰보기 까다로움. HTTP endpoint + JSON 응답이 훨씬 다루기 쉬움 ainote 의 cert_mirror 는 **GitHub 를 대체하는 게 아니라 보조**합니다. fastlane match 는 그대로 git 에 push 하고, push 가 성공하면 그 직후 동일 commit 의 tarball 을 ainote 에도 업로드. 둘 다 살아있어야 정상 — 한쪽이 사라지면 다른 쪽으로 복구. ## 무엇이 다른가 | 측면 | GitHub (fastlane match 기본) | ainote cert_mirror | |------|-----------------------------|--------------------| | 저장 형태 | git refs, 파일별 | 단일 tarball (commit 단위) | | 인증 | SSH key / PAT | `Authorization: McpKey ` | | 무결성 | git sha | HMAC-SHA256 (별도 secret) | | 권한 모델 | repo 전체 collaborator | 사용자별 `UserMirrorCredential` 1개 | | 사이즈 한도 | 100MB/file, 5GB/repo | 50MB/blob, 5GB/user (예정) | | 다운로드 | git clone | HTTP GET (JSON metadata + 바이너리) | ## 어떻게 동작하나 (한 사이클) ``` ┌────────────────────────────────────────────────────────────────┐ │ fastlane sync_signing │ │ 1. ASC API 로 cert / profile 발급 │ │ 2. seunghan91/apple-certs git push (1차 저장) │ │ 3. mirror_to_ainote 호출 ─┐ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────┐ │ │ │ git archive --format=tar.gz HEAD │ │ │ │ → streaming HMAC-SHA256 (8KB chunk)│ │ │ │ → POST /api/cert_mirror │ │ │ │ Authorization: McpKey │ │ │ │ X-Mirror-Signature: sha256=... │ │ │ │ X-Commit-Sha: <40 hex> │ │ │ │ Content-Type: application/octet-stream │ │ │ └────────────────┬────────────────────┘ │ │ │ │ └────────────────────────────┼────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────┐ │ ainote_server │ │ ┌──────────────────────────────┐ │ │ │ Api::CertMirrorController │ │ │ │ - McpKey 인증 │ │ │ │ - HMAC 재계산 후 비교 │ │ │ │ - 50MB 초과 시 inline 413 │ │ │ │ - tempfile → storage_path │ │ │ └──────────────┬───────────────┘ │ │ │ │ │ ▼ │ │ CertMirrorBlob (PostgreSQL) │ │ - commit_sha, byte_size, sha256 │ │ - uploaded_at, uploaded_by │ │ - storage_path → 디스크 또는 S3 │ └────────────────────────────────────┘ ``` ## 신뢰 모델 - **수신자(ainote)** 는 token 으로 사용자 인증, HMAC 으로 무결성 검증. body 자체는 fastlane match 가 이미 `MATCH_PASSWORD` 로 암호화한 상태라 ainote 가 평문 cert 를 만지지 않습니다. - **발신자(클라이언트)** 는 `MATCH_PASSWORD` 만 분실 안 하면 안전 — repo 도 mirror 도 둘 다 털려도 평문 cert 추출 불가. - **자기 자신(ainote 운영자)** 에게도 cert 평문은 안 보임. UserMirrorCredential `hmac_secret` 은 non-deterministic 암호화, `token` 은 deterministic (검색용). ## AI agent 통합 시 추천 패턴 1. 첫 실행 — 사용자에게 `ainote-cert-mirror issue` (또는 [api 페이지](/cert-mirror/api#발급-계정당-1개) 의 curl) 안내. CLI 가 Keychain 저장까지 자동 2. fastlane 워크플로우의 마지막에 [mirror_to_ainote 패턴](/cert-mirror/fastlane) 호출 3. 주기적으로 `ainote-cert-mirror verify` 호출 (heartbeat — endpoint 200 + Keychain present + blob count) 4. 새 dev 환경 셋업 시 [setup-guide](/cert-mirror/setup-guide) "다른 mac 에서 같은 계정 사용" 섹션 따라가면 끝 5. 회전 (`issue --rotate`) 후에는 반드시 fastlane sync_signing 재실행 — 기존 blob 무효화됨 ## 다음 - [API 레퍼런스](/cert-mirror/api) — 엔드포인트 / 인증 / 에러 코드 - [fastlane 통합 예시](/cert-mirror/fastlane) — Fastfile drop-in 스니펫 - [사용자 메모리 / dev docs 와 차이](/memory/overview) — 일반 메모리와 다른 점 ## 알려진 제약 - `UserMirrorCredential` 은 계정당 1개. 회전은 기존 entry 삭제 + 재생성 (자세한 절차는 [api 인증 섹션](/cert-mirror/api#인증)) - blob 영구 보관 — soft delete 지원 안 함 (이력 보존 정책상) - 한 계정당 5GB 정도까지를 권장 (강제 quota 는 미구현) - Render edge WAF 가 default `Ruby`/`curl` User-Agent 를 차단. 클라이언트는 custom User-Agent 명시 필수 (`ainote-fastlane-mirror/1.0` 등) --- # Cert Mirror — 5분 셋업 가이드 신규 user 가 0 에서 cert mirror 백업이 동작하기까지의 끝-to-끝 절차. CLI 스크립트 [`ainote-cert-mirror`](/cert-mirror/cli) 가 거의 모든 작업을 자동화하므로 실제 명령은 3-4 줄이면 끝납니다. ## 사전 조건 - macOS (Keychain 저장용. Linux/CI 환경은 [api 페이지](/cert-mirror/api#인증) 직접 호출 패턴 참조) - ainote 계정 + primary API key (`UserMcpKey`) - (선택) fastlane match repo — cert tarball 업로드 대상 ## Step 1. CLI 스크립트 설치 ```bash mkdir -p ~/scripts curl -fsSL https://api.ainote.dev/scripts/ainote-cert-mirror.sh -o ~/scripts/ainote-cert-mirror.sh chmod +x ~/scripts/ainote-cert-mirror.sh ``` (아직 호스팅 안 됨 — 임시로 `~/ainote-docs/scripts/ainote-cert-mirror.sh` 에서 복사하거나 [cli 페이지](/cert-mirror/cli) 의 전문을 저장) PATH 에 `~/scripts` 추가: ```bash echo 'export PATH="$HOME/scripts:$PATH"' >> ~/.zshrc source ~/.zshrc ``` ## Step 2. AINOTE_API_KEY env 등록 본인 ainote primary API key (`UserMcpKey.key` — 64-hex 또는 `User.api_key` — 24-char) 를 환경변수로: ```bash echo 'export AINOTE_API_KEY=""' >> ~/.zshrc source ~/.zshrc ``` 📍 API key 가 어디 있는지 모르면 ainote 웹 설정 → API Keys (또는 `/api/me/mcp_keys` 호출). ⚠️ **계정 정합성**: 여러 ainote 계정을 가진 경우 (예: 메일 계정별 분리), `AINOTE_API_KEY` 는 **mirror 를 만들 그 계정의 키**여야 합니다. 다른 계정의 키로 `issue` 호출하면 그 계정에 새 credential 이 생성됩니다 (의도치 않은 분기). `ainote-cert-mirror show` 로 현재 env 의 키 길이 확인 가능. ## Step 3. Credential 발급 + Keychain 저장 (1 명령) ```bash ainote-cert-mirror issue ``` 기대 출력: ``` ℹ️ Calling POST https://api.ainote.dev/api/cert_mirror/credentials (rotate=false) ✅ Credential created. token+hmac_secret saved to Keychain (services: "ainote mirror token", "ainote mirror hmac"). ``` 내부 동작: 1. `POST /api/cert_mirror/credentials` (auth = UserMcpKey) 2. 서버가 `UserMirrorCredential` 생성 → token + hmac_secret 반환 3. macOS Keychain 에 2개 entry 저장 (`ainote mirror token`, `ainote mirror hmac`) 4. iCloud Keychain 활성화돼있으면 다른 mac 으로 자동 동기화 ## Step 4. 검증 ```bash ainote-cert-mirror verify ``` 기대: ``` ✅ Endpoint https://api.ainote.dev/api/cert_mirror reachable (200) ✅ Keychain: token (64 chars) + hmac (64 chars) present ✅ Blobs uploaded: 0 ``` 여기까지 오면 endpoint + 인증이 정상. blob 0 개는 정상 (아직 아무것도 안 올림). ## Step 5. fastlane match 워크플로우 통합 [fastlane 통합 페이지](/cert-mirror/fastlane) 의 `mirror_to_ainote` private_lane 을 본인 Fastfile 에 추가. env 셋업: ```bash export MATCH_PASSWORD="$(security find-generic-password -s ' match' -w)" export AINOTE_MIRROR_TOKEN="$(security find-generic-password -s 'ainote mirror token' -w)" export AINOTE_MIRROR_HMAC_SECRET="$(security find-generic-password -s 'ainote mirror hmac' -w)" bundle exec fastlane sync_signing ``` 첫 mirror 성공 후 다시 검증: ```bash ainote-cert-mirror list ``` ```json { "blobs": [ { "commit_sha": "...", "byte_size": 60781, "uploaded_at": "..." } ] } ``` ## 다른 mac 에서 같은 계정 사용 iCloud Keychain 동기화 가정: 1. 다른 mac 에 `~/scripts/ainote-cert-mirror.sh` 설치 (Step 1) 2. `AINOTE_API_KEY` env 등록 (Step 2 — 같은 키) 3. `ainote-cert-mirror verify` 만 실행. Keychain 동기화돼있으면 token/hmac 이미 있음. `No mirror token in Keychain` 오류 = iCloud sync 안 됐거나 비활성. 둘 중: - 잠시 기다리고 재시도 (보통 2-5분) - `ainote-cert-mirror issue` 호출 → 기존 token 받아 Keychain 에 저장 (hmac_secret 은 못 받음 — 그럴 때는 `--rotate` 필요, 단 기존 blob 전부 무효화됨) ## 회전 (rotation) `hmac_secret` 분실 시 또는 정기 회전 (12개월 권장): ```bash ainote-cert-mirror issue --rotate ``` ⚠️ 기존 blob 들은 HMAC 검증 실패하므로 fastlane 워크플로우 한번 더 돌려서 재업로드: ```bash bundle exec fastlane sync_signing ``` ## 폐기 ainote 계정 자체는 살리되 cert mirror 만 정리: ```bash ainote-cert-mirror revoke ``` 확인 prompt 후: - 서버: `UserMirrorCredential` 삭제 (`CertMirrorBlob` 들은 남지만 새 credential 받기 전까지 unreachable) - 로컬: Keychain 2개 entry 삭제 ## 함정 | 증상 | 원인 | 해결 | |---|---|---| | `AINOTE_API_KEY ... not set` | env 누락 | `~/.zshrc` 에 export 추가, `source ~/.zshrc` | | `HTTP 403 Forbidden` | Render WAF (default UA) | 스크립트가 자동 처리 — 발생하면 `~/.zshrc` 의 alias 가 cert-mirror 명령을 override 하는지 확인 | | `HTTP 401 unauthorized` | AINOTE_API_KEY 가 잘못된 키 또는 expire | ainote 설정에서 새 API key 발급 | | `existing` 응답 + Keychain 에 hmac 없음 | hmac_secret 분실 (서버에선 재조회 불가) | `ainote-cert-mirror issue --rotate` (기존 blob 재업로드 비용 발생) | | iCloud Keychain 동기화 지연 | macOS 가 sync 못 따라잡음 | 2-5분 대기 후 재시도, 그래도 안 되면 `issue --rotate` | ## 다음 - [CLI 명령 레퍼런스](/cert-mirror/cli) — 모든 subcommand + 옵션 - [fastlane 통합](/cert-mirror/fastlane) — Fastfile drop-in - [API 레퍼런스](/cert-mirror/api) — 직접 호출 (Linux/CI 환경) --- # 환경변수 / .env 동기화 여러 프로젝트의 `.env` 파일을 ainote + macOS Keychain 으로 안전 동기화. ## 설계 ``` .env 파일 ├─ 실제 값 → macOS Keychain (service: dev-env/{project}) │ └─ iCloud Keychain 으로 맥미니 ↔ 맥북 자동 sync └─ 키 목록 → ainote ({project}-env-ref.md) └─ 키 이름 + 타입(secret/config) + Keychain 서비스명 ``` **왜 분리**: - 값은 Keychain (OS 레벨 암호화 + iCloud 동기화) - 참조만 ainote (어떤 키 있는지 파악, 새 기기 셋업 가이드) ## 스크립트 사용 `~/scripts/ainote-env-sync.sh` (이미 셋업됨, 미설치라면 [GitHub repo](https://github.com/seunghan91/ainote-tools)). ### Push: .env → Keychain + ainote ```bash ainote-env-sync.sh push tennis-bracket ``` 스크립트 동작: 1. `~/tennis_bracket/.env` 읽음 2. 각 KEY=VALUE 를 Keychain 에 저장 (service: `dev-env/tennis-bracket`, account: KEY) 3. 키 목록 (이름만) 을 ainote 에 `tennis-bracket-env-ref.md` 로 저장 ### Pull: Keychain → .env (새 기기) ```bash ainote-env-sync.sh pull tennis-bracket ``` 스크립트 동작: 1. ainote 에서 `tennis-bracket-env-ref.md` 받음 → 키 목록 파싱 2. 각 키를 Keychain 에서 read (iCloud Keychain 으로 이미 sync 됨) 3. `~/tennis_bracket/.env` 생성 ### 일괄 ```bash ainote-env-sync.sh push-all # 등록된 모든 프로젝트 ainote-env-sync.sh pull-all ainote-env-sync.sh list # 등록된 프로젝트 목록 ``` ## 등록된 프로젝트 (예) ```bash ainote-env-sync.sh list ``` ``` tennis-bracket 12 keys Last sync: 2026-05-07 launchcrew 18 keys Last sync: 2026-05-06 keeps 8 keys Last sync: 2026-05-05 triphelper 15 keys realpick 9 keys talkk 14 keys ax-admin 22 keys ainote-app 11 keys ``` ## ainote 에 저장되는 형식 `{project}-env-ref.md`: ```markdown --- name: tennis-bracket-env-ref type: reference ainote_sync: env-refs/tennis-bracket-env-ref.md --- # tennis-bracket .env Reference | Key | Type | Keychain Service | |-----|------|------------------| | RAILS_ENV | config | (env) | | DATABASE_URL | secret | dev-env/tennis-bracket | | TOSS_CLIENT_KEY | secret | dev-env/tennis-bracket | | TOSS_SECRET_KEY | secret | dev-env/tennis-bracket | | FIREBASE_PROJECT_ID | config | (env) | | ... | ``` 값은 ainote 에 안 저장됨. 이름만. ## 비밀 누출 방지 - ❌ `.env` 자체를 ainote 에 push 금지 (값 노출) - ❌ git 에 `.env` 커밋 금지 (`.gitignore` 에 `.env*` 추가) - ✅ `.env.example` 만 git 에 커밋 (값 placeholder) - ✅ 정기적으로 키 회전 (분기마다) ## 새 기기 셋업 (5분) ```bash # 1. ainote-env-sync.sh 받기 curl -O https://raw.githubusercontent.com/seunghan91/ainote-tools/main/ainote-env-sync.sh chmod +x ainote-env-sync.sh # 2. ainote 환경변수 export AINOTE_API_KEY="..." # 3. iCloud Keychain 켜져 있는지 확인 (시스템 설정 → Apple ID) # 4. 일괄 pull ./ainote-env-sync.sh pull-all ``` → 모든 프로젝트의 `.env` 가 자기 자리에 복원. ## 다음 - [shell 함수 셋업](/cli/shell-function) - [`.env` 보안 패턴 예시](/examples/env-sync) - [데이터 내보내기](/guide/data-export) --- # ainote 커맨드라인 ainote 는 별도의 `ainote` CLI 명령을 제공하지 않습니다 — 대신 두 가지 방법으로 터미널에서 사용: 1. **shell 함수 (curl wrapper)** — 가장 간단. 의존성 없음. 권장. 2. **`@ainote/mcp` 패키지의 두 바이너리** — `ainote-mcp` (stdio MCP 서버) + `ainote-mcp-http` (SSE 브리지) ## 빠른 사용 ### shell 함수 한 번 정의 후 ```bash ainote list_tasks '{"status":"pending","limit":10}' ainote create_task '{"content":"테스트"}' ainote pull_dev_docs '{}' ``` `ainote` 는 함수 이름 — 별칭이라 자유롭게 변경 가능. [shell 함수 등록](/cli/shell-function) 참고. ### `@ainote/mcp` 바이너리 ```bash npm install -g @ainote/mcp # stdio MCP 서버 (Claude Desktop 이 spawn 해서 사용) ainote-mcp # SSE 브리지 (ChatGPT) — AINOTE_API_URL + AINOTE_API_KEY env 필요 ainote-mcp-http ``` [설치 가이드](/cli/install) (env var 셋업 포함). ## 주요 워크플로우 | 작업 | 명령 (shell 함수 기준) | |------|----------------------| | 오늘 할 일 | `ainote list_tasks '{"due_today":true}'` | | 새 태스크 | `ainote create_task '{"content":"회의"}'` | | 모든 메모리 받기 | `ainote pull_dev_docs '{}'` | | 카테고리별 받기 | `ainote pull_dev_docs '{"category":"claude"}'` | | .env push | `ainote-env-sync push tennis-bracket` (별도 스크립트) | ## 환경 설정 ```bash # ~/.zshrc export AINOTE_API_URL="https://api.ainote.dev" export AINOTE_API_KEY="h7Axq9XPsDTD2qr5yqtcCSaQ..." ``` ## 다음 - [설치](/cli/install) - [shell 함수 (curl wrapper)](/cli/shell-function) - [환경변수 / .env 동기화](/cli/env-sync) --- # 설치 `@ainote/mcp` 패키지는 두 개의 바이너리를 설치합니다: | 바이너리 | 용도 | |---------|------| | `ainote-mcp` | stdio MCP 서버 (Claude Desktop 등) | | `ainote-mcp-http` | SSE 브리지 (ChatGPT 등) | ::: info `ainote` 명령은 없음 별도 CLI 진입점은 제공하지 않습니다. 자유롭게 호출하려면 [shell 함수 (curl wrapper)](/cli/shell-function) 사용 권장. ::: ## 설치 ```bash npm install -g @ainote/mcp ``` 설치 확인: ```bash which ainote-mcp ainote-mcp-http # /Users/.../bin/ainote-mcp # /Users/.../bin/ainote-mcp-http ``` 요구사항: Node 18+. ## 환경변수 `~/.zshrc` 또는 `~/.bashrc`: ```bash export AINOTE_API_URL="https://api.ainote.dev" export AINOTE_API_KEY="h7Axq9XPsDTD2qr5yqtcCSaQ..." ``` reload: ```bash source ~/.zshrc ``` ::: warning AINOTE_API_URL 은 반드시 설정 `@ainote/mcp` 의 빌트인 기본값은 아직 구 백엔드 (`https://ainote-5muq.onrender.com`) 를 가리킵니다. env 를 비우면 패키지가 조용히 구 호스트로 붙어서 신규 기능/데이터가 안 보일 수 있어요. 위 export 두 줄을 꼭 설정하세요. ::: ::: tip stdio 모드는 Claude Desktop 이 직접 spawn `ainote-mcp` 자체는 stdio 프로토콜이라 사람이 직접 실행하면 멈춰있는 것처럼 보임 (정상). Claude Desktop 같은 MCP 클라이언트에서 spawn 해서 사용. ::: ## SSE 브리지 (ChatGPT 용) ```bash export AINOTE_API_URL="https://api.ainote.dev" # 빌트인 default 는 구 onrender 호스트 export AINOTE_API_KEY="..." export AINOTE_MCP_HTTP_PORT=8765 # 기본 3030 ainote-mcp-http ``` 자세히: [ChatGPT 연결](/mcp/chatgpt). ## Self-host 인스턴스 `AINOTE_API_URL` 만 자체 인스턴스 URL 로 변경: ```bash export AINOTE_API_URL="https://my-ainote.internal" ``` ## 업데이트 ```bash npm update -g @ainote/mcp ``` ## 제거 ```bash npm uninstall -g @ainote/mcp ``` `~/.zshrc` 에서 `AINOTE_*` 라인 삭제. ## 다음 - [shell 함수 (curl wrapper, 의존성 없음)](/cli/shell-function) - [Claude Desktop 연결](/mcp/claude-desktop) - [ChatGPT 연결 (SSE 브리지)](/mcp/chatgpt) --- # shell 함수 (curl wrapper) Node 설치 없이 ainote 호출하는 가장 간단한 방법. ## 셋업 `~/.zshrc` (또는 `~/.bashrc`) 에 추가: ```bash # ainote MCP 환경 export AINOTE_API_URL="https://api.ainote.dev" export AINOTE_API_KEY="h7Axq9XPsDTD2qr5yqtcCSaQ..." # ainote 함수 — JSON-RPC 호출 wrapper (이름은 자유롭게 변경 가능) ainote() { local method="$1" local args="${2:-{\}}" curl -s -X POST "$AINOTE_API_URL/api/mcp" \ -H "Content-Type: application/json" \ -H "Authorization: McpKey $AINOTE_API_KEY" \ -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"${method}\",\"arguments\":${args}}}" \ | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('result',{}).get('content',[{}])[0].get('text','') or json.dumps(d,indent=2,ensure_ascii=False))" 2>/dev/null } ``` reload: ```bash source ~/.zshrc ``` ## 사용 ### 태스크 ```bash # 오늘 할 일 ainote list_tasks '{"due_today":true,"limit":20}' # 새 태스크 (필수: content) ainote create_task '{"content":"테스트","due_date":"2026-05-08T10:00:00+09:00"}' # 완료 처리 (completed_at 으로 ISO 시각) ainote update_task '{"id":"","completed_at":"2026-05-08T10:35:00+09:00"}' # 삭제 (id 는 UUID) ainote delete_task '{"id":""}' ``` ### 메모리 ```bash # 카테고리별 목록 ainote list_dev_docs '{"category":"claude","limit":50}' # 단일 조회 ainote get_dev_doc '{"title":"global-claude-guidelines.md"}' # 검색 ainote list_dev_docs '{"search":"firebase"}' # 일괄 복원 (새 기기) ainote pull_dev_docs '{}' ``` ### Vault / Sync ::: tip ✅ 라이브 (서버) `vault_*` / `sync_*` 도구는 `api.ainote.dev` 서버에서 동작합니다 — JSON-RPC `POST /api/mcp` 또는 [`@ainote/sdk`](/build/sdk)로 호출. (`@ainote/mcp` npm 구버전엔 번들이 안 됐을 수 있음.) [vault 개요](/vault/overview) / [sync 개요](/sync/overview) 참고. ::: ## 헬퍼 함수 자주 쓰는 패턴 wrap: ```bash # 오늘 할 일 (alias) ainote-today() { ainote list_tasks '{"due_today":true}'; } # Push (파일 → ainote) ainote-push() { local file="$1" local title="$2" local category="${3:-docs}" local content content=$(python3 -c "import sys,json; print(json.dumps(open('$file').read()))") local local_path local_path=$(realpath "$file") ainote update_dev_doc "{\"title\":\"$title\",\"content\":$content,\"local_path\":\"$local_path\",\"subcategory\":\"$category\"}" \ || ainote create_dev_doc "{\"title\":\"$title\",\"content\":$content,\"local_path\":\"$local_path\",\"subcategory\":\"$category\"}" } # 사용 ainote-push ~/CLAUDE.md global-claude-guidelines.md claude ainote-push ~/tennis_bracket/CLAUDE.md tennis-bracket-claude.md claude ``` ## 디버그 모드 원시 응답 보고 싶을 때: ```bash ainote-raw() { curl -sv -X POST "$AINOTE_API_URL/api/mcp" \ -H "Content-Type: application/json" \ -H "Authorization: McpKey $AINOTE_API_KEY" \ -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"$1\",\"arguments\":${2:-{\}}}}" } ainote-raw list_tasks '{}' # 헤더 + 본문 모두 ``` ## .env 보호 키를 `~/.zshrc` 직접 X — `~/.zshenv` 또는 별도 파일: ```bash # ~/.zshenv (gui app 도 읽음) export AINOTE_API_KEY="..." # ~/.zshrc 또는 별도 [ -f ~/.ainote-env ] && source ~/.ainote-env ``` `~/.ainote-env` 는 chmod 600: ```bash chmod 600 ~/.ainote-env ``` ## 다음 - [환경변수 / .env 동기화 자동화](/cli/env-sync) - [Node CLI 설치](/cli/install) - [전체 도구 카탈로그](/reference/) --- # Claude Code 자연어 사용 ainote MCP 등록 후 일상 워크플로우. ## 아침 루틴 ``` 오늘 할 일 보여줘 ``` Claude → `list_tasks {due_today: true, status: pending}` → 대답: ``` 오늘 할 일 5개: ⭐ 강남역 미팅 10:00 (30분 전 알림) 회의 준비 09:30 주간 회고 작성 14:00 코드 리뷰 PR #123 종일 운동 (헬스장) 18:00 ``` ## 작업 중 — 빠른 메모 ``` 이거 메모리에 저장해줘: "Rails 8 의 Solid Queue 는 PUMA worker 안에서도 돌릴 수 있다 (SOLID_QUEUE_IN_PUMA=true)" ``` Claude → `create_dev_doc`: ```json { "title": "feedback-rails8-solid-queue-puma.md", "category": "memory", "type": "feedback", "content": "..." } ``` 다음 세션에서 자동으로 이 메모리 사용 가능. ## CLAUDE.md 업데이트 ``` launchcrew 의 CLAUDE.md 에 "Stripe 방식 snake_case 절대 변경 금지" 추가하고 ainote 에 동기화 ``` Claude: 1. `~/launchcrew/CLAUDE.md` 읽음 2. 새 섹션 추가 3. 파일 저장 4. `update_dev_doc` 으로 ainote push ## 태스크 일괄 추가 ``` 다음주 월요일 회의: - 9시 임원 미팅 - 10시 디자인 리뷰 - 14시 회고 ``` Claude → `create_task` 3번 (또는 batch): ``` ✅ 3개 태스크 생성: - 임원 미팅 (월 09:00) - 디자인 리뷰 (월 10:00) - 회고 (월 14:00) ``` ## 카테고리 자동 매칭 ``` 이번주 운동 3번 — 화/목/토 7시 ``` Claude: 1. `list_categories` → "운동" id 찾음 2. `create_task` 3번, 각각 `category_id: 3` ``` ✅ 3개 운동 태스크 생성됨 (카테고리: 운동) ``` ## 컨텍스트 활용 이전 대화에서 저장된 메모리 자동 활용: ``` 사용자: 그 PostgreSQL 연결 문제 어떻게 풀었지? ``` Claude → `list_dev_docs {search: "postgresql"}` → 메모리 찾음 → 대답: ``` 2026-04-15 메모리에 따르면 — Render Singapore 리전이 idle 시 sleep 되어 첫 호출 5초 wait 발생. 해결: cron 으로 1분마다 ping. ``` ## Vault 활용 ``` personal vault 의 daily/2026-05-07.md 보여줘 ``` Claude: 1. `vault_connect_status` → vault clone 확인 2. 파일 직접 read (vault 가 로컬에 있음) 또는: ``` personal vault 에 오늘 일기 추가: "오후에 ainote docs 작업. 17개 도구 카탈로그 정리 완료." ``` Claude: 1. `vault_sync personal` (최신 상태) 2. `daily/2026-05-07.md` 파일 작성/append 3. `vault_sync personal` (push) ## 회고 (저녁) ``` 오늘 완료한 거 정리해서 일기에 추가 ``` Claude: 1. `list_tasks {status: completed, completed_within_days: 1}` 2. 마크다운 정리 3. `vault_sync` → `daily/2026-05-07.md` append ## 특이 패턴 ### 태스크 완료 자동 reminder ``` 강남역 미팅 끝나면 회고 메모 추가하라고 알려줘 ``` → Claude 가 `create_task` 의 후속 reminder 로 또 다른 태스크 만듦. ### 외부 시스템과 연결 ``` 이 슬랙 메시지 (붙여넣음) 를 launchcrew 메모리에 reference 로 저장 ``` → `create_dev_doc {type: "reference", category: "memory"}`. ## 다음 - [태스크 개요](/tasks/overview) - [메모리 / Dev Docs](/memory/overview) - [Telegram 으로 외부에서 추가](/examples/telegram) --- # CLAUDE.md 멀티프로젝트 관리 17개+ 프로젝트의 CLAUDE.md 를 일관성 있게 유지. ## 문제 ``` ~/CLAUDE.md ~/launchcrew/CLAUDE.md ← Stripe naming 규칙 ~/tennis_bracket/CLAUDE.md ← Stripe naming 규칙 (똑같이 복붙) ~/keeps/CLAUDE.md ← Stripe naming 규칙 ~/triphelper/CLAUDE.md ← Stripe naming 규칙 ... (17개 모두 같은 규칙 + 프로젝트 별 차이) ``` 전역 규칙 변경 시 17개 파일 동시 수정 → drift 발생. ## 해결 패턴 ### 패턴 A: 전역 + 프로젝트 분리 각 프로젝트 `CLAUDE.md` 시작 부분: ```markdown # {Project} Claude Guidelines ## Inherits from Global 이 파일은 ~/CLAUDE.md 의 전역 규칙을 따릅니다. 프로젝트 특유의 규칙만 아래 명시. ## Project-specific ### Tech Stack - Rails 8 + Inertia.js + Svelte 5 - ... ### Domain - ... ``` 전역 (`~/CLAUDE.md`) 에는 모든 프로젝트 공통: - API naming convention (Stripe snake_case) - 한국어 응답 - Apple 계정 매핑 - 디자인 시스템 토큰 - ... → 전역 변경 시 한 곳만 수정. ### 패턴 B: ainote 에 한 번 등록 후 sync 위 패턴 + ainote 로 source-of-truth 화: ```bash # 전역 + 17개 프로젝트 일괄 등록 ~/scripts/ainote-claude-md-init.sh ``` 스크립트 ([CLAUDE.md 통합 관리](/memory/claude-md) 참고): ```bash ainote-push ~/CLAUDE.md global-claude-guidelines.md claude for proj in ~/launchcrew ~/tennis_bracket ~/keeps ~/triphelper \ ~/realpick ~/talkk ~/krx_listing ~/krx_ai \ ~/mbti_luck ~/forfit ~/ax_admin ~/ainote-app ...; do if [ -f "$proj/CLAUDE.md" ]; then name=$(basename "$proj" | tr '_' '-') ainote-push "$proj/CLAUDE.md" "${name}-claude.md" claude fi done ``` 수정 워크플로우: ``` 1. 어디서든 CLAUDE.md 수정 (맥미니 또는 맥북) 2. ainote-push 실행 3. 다른 기기 cron 이 15분 뒤 pull ``` ## 새 프로젝트 추가 시 ```bash # 1. CLAUDE.md 작성 $EDITOR ~/new-project/CLAUDE.md # 2. ainote 등록 ainote-push ~/new-project/CLAUDE.md new-project-claude.md claude # 3. (선택) sync-all 스크립트에 추가 $EDITOR ~/scripts/ainote-claude-md-init.sh ``` ## 일관성 검증 매주 cron: ```bash ~/scripts/ainote-claude-md-audit.sh ``` 체크: - 모든 프로젝트 CLAUDE.md 가 ainote 에 등록됐나 - 로컬 ↔ ainote 내용 일치 - 어느 프로젝트가 stale (최근 변경 없음 = 사용 안 함 가능) 샘플 audit 출력: ``` ✅ 17/17 projects registered ✅ 16/17 in sync ⚠️ 1 drift detected: ~/legacy-app/CLAUDE.md (local) ≠ ainote (last push 2025-12-01) → 수동 검토 후 ainote-push 또는 삭제 ``` ## 디자인 토큰 / 코드 스타일 전역 `~/CLAUDE.md` 의 [vibe-design + IronAct](https://ainote.dev) 섹션: - 디자인 토큰 (Primary `#0000FF`, Border 0px 등) - Gestalt 원칙 - shadcn 패턴 → 프로젝트별 CLAUDE.md 에서는 "전역 따름" 만 명시. 새 프로젝트 시작 시: ```bash cat > ~/new-project/CLAUDE.md <<'EOF' # {Project} Claude Guidelines ## Inherits from ~/CLAUDE.md - 디자인: vibe-design 토큰 - API: Stripe snake_case - 언어: 한국어 ## Project-specific ### Tech Stack ... EOF ainote-push ~/new-project/CLAUDE.md new-project-claude.md claude ``` ## CLAUDE.md 충돌 같은 프로젝트 두 기기 동시 수정 → LWW (5분 임계 후). 복구: ```bash cd ~/.ainote-vault # vault 가 ainote git 미러일 때 git log -p dev/claude/launchcrew-claude.md git show :dev/claude/launchcrew-claude.md > /tmp/lost.md diff /tmp/lost.md ~/launchcrew/CLAUDE.md # 비교 후 머지 ``` ## 다음 - [CLAUDE.md 통합 관리](/memory/claude-md) - [맥미니 ↔ 맥북 동기화](/examples/multi-device) - [Cursor / Windsurf rules 같은 패턴](/memory/cursor-windsurf) --- # .env 보안 동기화 패턴 ## 핵심 원칙 ``` ┌─────────────────────────────────────────────────┐ │ 값 (secret) → macOS Keychain │ │ + iCloud Keychain (자동 sync) │ │ │ │ 키 목록 (참조) → ainote ({project}-env-ref.md) │ │ │ │ ❌ 값 자체를 ainote 에 저장 금지 │ │ ❌ git 에 .env 커밋 금지 │ └─────────────────────────────────────────────────┘ ``` ## 왜 이 분리 | 저장소 | 보안 | 동기화 | 검색 | |--------|------|-------|------| | ainote 에 값 | 약함 (키 노출 위험) | ✅ | ✅ | | Keychain 만 | 강함 (OS 암호화) | ✅ iCloud | ❌ | | **Keychain + ainote 참조** | 강함 | ✅ | ✅ (참조만) | ainote 는 어떤 키가 어디 있는지만 — 실제 값은 Keychain. ## 스크립트 사용 `~/scripts/ainote-env-sync.sh` ([설치 가이드](/cli/env-sync)). ### Push: .env → Keychain + ainote 참조 ```bash ainote-env-sync.sh push tennis-bracket ``` 내부 동작: 1. `~/tennis_bracket/.env` 읽음 2. 각 KEY=VALUE 를 `security add-generic-password` 로 Keychain 저장 - service: `dev-env/tennis-bracket` - account: KEY 이름 3. 키 목록 (이름만) 을 ainote 에 `tennis-bracket-env-ref.md` 로 push ### Pull: Keychain → .env (새 기기) ```bash ainote-env-sync.sh pull tennis-bracket ``` 내부 동작: 1. ainote 에서 `tennis-bracket-env-ref.md` 받음 → 키 목록 2. 각 키를 `security find-generic-password` 로 Keychain read 3. `~/tennis_bracket/.env` 생성 ## ainote 에 저장되는 형식 `tennis-bracket-env-ref.md`: ```markdown --- name: tennis-bracket-env-ref type: reference ainote_sync: env-refs/tennis-bracket-env-ref.md last_pushed: 2026-05-07T10:00:00Z device: macmini-2026-04 --- # tennis-bracket .env Reference | Key | Type | Source | Note | |-----|------|--------|------| | RAILS_ENV | config | env | development/production | | DATABASE_URL | secret | Keychain | postgres URL | | TOSS_CLIENT_KEY | secret | Keychain | TossPayments client | | TOSS_SECRET_KEY | secret | Keychain | TossPayments secret | | FIREBASE_PROJECT_ID | config | env | tennis-bracket-app | | AWS_ACCESS_KEY_ID | secret | Keychain | S3 | | ... | ... | ... | ... | ``` 값 없음. 이름 + 타입만. ## 새 기기 일괄 복원 (5분) ```bash # 1. iCloud Keychain 켜기 (시스템 설정) # 2. 스크립트 받기 brew install ainote-tools # 3. ainote 키 export AINOTE_API_KEY="..." # 4. 일괄 ainote-env-sync.sh pull-all ``` 응답: ``` ✓ ~/launchcrew/.env (18 keys from Keychain) ✓ ~/tennis_bracket/.env (12 keys) ... (12 projects) ✅ 12 .env files restored ``` ## 키 회전 분기마다: ```bash # 1. 새 키 발급 (서비스 사이트) TOSS_SECRET_KEY="" # 2. .env 수정 $EDITOR ~/tennis_bracket/.env # 3. push (Keychain 갱신 + ainote ref 갱신 — last_pushed 만 변경) ainote-env-sync.sh push tennis-bracket ``` ## 비밀 누출 시 ```bash # 1. Keychain 즉시 삭제 security delete-generic-password -s "dev-env/tennis-bracket" -a "TOSS_SECRET_KEY" # 2. ainote 참조에서도 표시 $EDITOR ~/tennis_bracket/.env # KEY 자체 삭제 ainote-env-sync.sh push tennis-bracket ``` 서비스 콘솔에서 키 폐기 + 새 발급. ## 등록된 프로젝트 (예시) ```bash $ ainote-env-sync.sh list tennis-bracket 12 keys Last sync: 2026-05-07 launchcrew 18 keys Last sync: 2026-05-06 keeps 8 keys Last sync: 2026-05-05 triphelper 15 keys realpick 9 keys talkk 14 keys ax-admin 22 keys ainote-app 11 keys krx-listing 19 keys krx-ai 13 keys mbti-luck 7 keys forfit 6 keys ``` ## 비교: 다른 솔루션 | | ainote-env-sync | 1Password CLI | doppler | direct iCloud | |---|---|---|---|---| | iCloud Keychain | ✅ free | △ | ❌ | ✅ | | 키 목록 검색 | ✅ ainote | ✅ | ✅ | ❌ | | 무료 | ✅ | △ | ⚠️ free tier | ✅ | | 프로젝트별 .env | ✅ | △ vault | ✅ env | ❌ | | 자동화 친화 | ✅ shell | ✅ CLI | ✅ CLI | ❌ | ## 다음 - [CLI / shell 함수](/cli/shell-function) - [환경변수 / .env 동기화 자세히](/cli/env-sync) - [데이터 내보내기 / 삭제](/guide/data-export) --- # 맥미니 ↔ 맥북 메모리 동기화 실전 시나리오: 두 Mac 에서 같은 메모리/CLAUDE.md 사용. ## 시나리오 - 사무실: **맥미니** (24/7 켜져있음) - 카페/이동: **맥북에어** (간헐적 사용) - 양쪽에서 17개 프로젝트 작업, CLAUDE.md / 메모리 / `.cursorrules` 모두 동기화 필요 ## 1회 셋업 (맥미니 — 메인) ### A. CLI / 환경 ```bash # ~/.zshrc export AINOTE_API_URL="https://api.ainote.dev" # host only — ainote() 함수가 /api/mcp 를 append export AINOTE_API_KEY="" ainote() { # ... shell 함수 } source ~/.zshrc ``` ### B. 모든 CLAUDE.md 일괄 등록 ```bash ainote-push() { ... } # 정의 # 전역 ainote-push ~/CLAUDE.md global-claude-guidelines.md claude # 17개 프로젝트 for proj in ~/launchcrew ~/tennis_bracket ~/keeps ~/triphelper \ ~/realpick ~/talkk ~/krx_listing ~/krx_ai ...; do if [ -f "$proj/CLAUDE.md" ]; then name=$(basename "$proj" | tr '_' '-') ainote-push "$proj/CLAUDE.md" "${name}-claude.md" claude fi done # .cursorrules 들 for proj in ~/launchcrew ~/krx_ai; do [ -f "$proj/.cursorrules" ] || continue name=$(basename "$proj" | tr '_' '-') ainote-push "$proj/.cursorrules" "${name}-cursorrules.md" cursor done ``` ### C. 메모리 등록 ```bash for md in ~/.claude/projects/*/memory/*.md; do proj=$(basename $(dirname $(dirname "$md")) | sed 's/-Users-seunghan-//' | sed 's/-Users-seunghan/global/') fname=$(basename "$md" .md) title="${proj}-${fname}.md" ainote-push "$md" "$title" memory done ``` ### D. 정기 cron (맥미니) ```bash # crontab -e */15 * * * * /Users/seunghan/scripts/ainote-sync-all.sh >> /tmp/ainote.log 2>&1 ``` ## 2회 셋업 (맥북 — 새 기기) ### A. 같은 CLI 셋업 ```bash # ~/.zshrc 동일하게 # 단 AINOTE_API_KEY 는 새로 발급 (맥북용) ``` 키 발급: ```bash ainote login_and_get_key '{ "email":"me@example.com", "password":"...", "name":"Seunghan" }' ``` ### B. 일괄 pull ```bash # 모든 dev_doc 받기 ainote pull_dev_docs '{}' ``` 응답: ``` ✓ /Users/seunghan/CLAUDE.md (created) ✓ /Users/seunghan/launchcrew/CLAUDE.md (created) ✓ /Users/seunghan/launchcrew/.cursorrules (created) ✓ /Users/seunghan/.claude/projects/-Users-seunghan-launchcrew/memory/MEMORY.md (created) ... (53 files) ✅ 53 dev_docs pulled, 0 errors ``` ### C. 같은 cron 설정 ## 일상 사용 ### 맥미니에서 CLAUDE.md 수정 ```bash $EDITOR ~/launchcrew/CLAUDE.md ainote-push ~/launchcrew/CLAUDE.md launchcrew-claude.md claude ``` ### 15분 뒤 맥북에서 자동 받음 cron 이 `pull_dev_docs '{"since":""}'` 실행 → 변경된 파일만 갱신. ### 또는 Claude 에서 명시적 맥북 Claude: ``` ainote 에서 변경된 CLAUDE.md 다 받아줘 ``` ## 충돌 시나리오 ### A. 맥미니 14:00 / 맥북 14:01 동시 수정 같은 파일 → 5분 임계 안 → conflict 디렉토리: ``` ~/.claude/ainote-sync/conflicts/ └── 2026-05-07T14-01-00__claude-md__launchcrew-claude.md.diff ``` 다음 sync 막힘. 사용자가 수동: ```bash cat ~/.claude/ainote-sync/conflicts/...diff $EDITOR /tmp/merged.md # 양쪽 합침 ainote sync_resolve launchcrew-claude.md --merge /tmp/merged.md ``` ### B. 맥미니 14:00 / 맥북 16:00 5분 이상 → LWW → 맥북 (HLC 더 큼) 이김. 맥미니 변경은 git history 에: ```bash cd ~/.ainote-vault/projects/launchcrew git log -p CLAUDE.md ``` ## 추가 디바이스 (iPad) iPad 는 stdio/SSE 어려움 → ChatGPT/Claude iOS 앱 + ainote 의 mobile app 사용. ainote iOS 앱은 자체 sync 처리 — manifest 기반 시스템과 별개 (REST API). ## 비용 - 맥미니: 24/7 cron → 96 호출/일 (sync_list) - 맥북: 15분마다 → 96 호출/일 - 합 192/20000 (free tier 의 1%) 여유 충분. ## 다음 - [새 디바이스 셋업](/examples/new-device) - [.env 보안 동기화 패턴](/examples/env-sync) - [충돌 해결 자세히](/sync/conflicts) --- # 새 디바이스 셋업 (5분) 새 Mac 받았다. ainote 모든 데이터 + 17개 프로젝트 CLAUDE.md + 메모리 + `.env` 복원. ## 시간 분배 - 1분 — 환경 setup (zsh 함수 + env) - 30초 — 키 발급 - 2분 — `pull_dev_docs` 일괄 복원 - 30초 — `.env` Keychain 복원 ::: tip ✅ vault_clone 라이브 (서버) 서버에서 동작합니다 — 단 클론하려면 **연결된 git-backed vault**가 필요합니다(미연결 vault는 클론 대상 없음). 추가 1분. ::: ## 0. Prereq - macOS 최신 - iCloud Keychain ON (Apple ID 로그인 → 시스템 설정 확인) - Homebrew + zsh ## 1. CLI 환경 (1분) ```bash # ainote shell 함수 다운로드 curl -O https://raw.githubusercontent.com/seunghan91/ainote-tools/main/setup-shell.sh bash setup-shell.sh ``` 이 스크립트가 `~/.zshrc` 에 `ainote()` 함수 + `AINOTE_API_URL` 추가. 수동: ```bash cat >> ~/.zshrc <<'EOF' export AINOTE_API_URL="https://api.ainote.dev" # host only — 아래 함수가 /api/mcp 를 append ainote() { local method="$1" local args="${2:-{\}}" curl -s -X POST "$AINOTE_API_URL/api/mcp" \ -H "Content-Type: application/json" \ -H "Authorization: McpKey $AINOTE_API_KEY" \ -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"${method}\",\"arguments\":${args}}}" \ | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('result',{}).get('content',[{}])[0].get('text','') or json.dumps(d,indent=2,ensure_ascii=False))" } EOF source ~/.zshrc ``` ## 2. 키 발급 (30초) ```bash # 인증 헤더 없이 호출 (signup/login 은 헤더 불필요) curl -X POST "$AINOTE_API_URL/api/mcp" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc":"2.0","id":1,"method":"tools/call", "params":{"name":"login_and_get_key","arguments":{ "email":"me@example.com", "password":"...", "name":"Seunghan" }} }' ``` 응답에서 `mcp_key` 복사 → `~/.zshrc`: ```bash export AINOTE_API_KEY="h7Ax..." source ~/.zshrc ``` ## 3. dev_docs 일괄 (2분) ```bash ainote pull_dev_docs '{}' ``` 응답: ``` ✓ /Users/seunghan/CLAUDE.md (created) ✓ /Users/seunghan/.claude/PERSONAS.md (created) ✓ /Users/seunghan/.claude/ORCHESTRATOR.md (created) ✓ /Users/seunghan/launchcrew/CLAUDE.md (created) ← 디렉토리 자동 생성 ✓ /Users/seunghan/tennis_bracket/CLAUDE.md (created) ... (53 files) ✅ 53 dev_docs pulled, 0 errors ``` ::: warning 디렉토리 자동 생성 `pull_dev_docs` 가 `local_path` 의 부모 디렉토리도 자동 mkdir. 단 권한 문제 시 skipped. ::: ## 4. Vault 클론 (1분, 선택) ::: tip ✅ 라이브 (서버, 연결된 vault 필요) `vault_clone`은 서버에서 동작합니다. 연결된 git-backed vault가 있으면: ```bash # 목록 확인 ainote vault_list '{}' # 필요한 거 clone ainote vault_clone '{"name":"personal","destination":"/Users/seunghan/notes/personal"}' ainote vault_clone '{"name":"work","destination":"/Users/seunghan/notes/work"}' ``` ::: ## 5. .env Keychain 복원 (30초) ```bash # 스크립트 다운로드 curl -O https://raw.githubusercontent.com/seunghan91/ainote-tools/main/ainote-env-sync.sh chmod +x ainote-env-sync.sh sudo mv ainote-env-sync.sh /usr/local/bin/ # 일괄 pull (iCloud Keychain 이 이미 sync 됨) ainote-env-sync.sh pull-all ``` 응답: ``` ✓ ~/launchcrew/.env (12 keys) ✓ ~/tennis_bracket/.env (8 keys) ✓ ~/keeps/.env (15 keys) ... (12 projects) ✅ 12 .env files restored from Keychain ``` ## 6. Claude Code MCP 등록 `~/.claude.json`: ```json { "mcpServers": { "ainote": { "type": "http", "url": "https://api.ainote.dev/api/mcp", "headers": { "Authorization": "McpKey YOUR_KEY" } } } } ``` Claude Code 시작 → `/mcp` 확인. ## 7. (선택) sync 자동화 ```bash # crontab -e */15 * * * * /usr/local/bin/ainote-sync-all.sh >> /tmp/ainote.log 2>&1 ``` 또는 launchd plist (macOS 권장). ## 검증 체크리스트 ```bash ls ~/CLAUDE.md # 있어야 함 ls ~/launchcrew/CLAUDE.md ls ~/tennis_bracket/CLAUDE.md cat ~/launchcrew/.env | head # 키 복원됨 ainote whoami # 키 정상 ainote list_dev_docs '{"limit":5}' # 응답 옴 ``` ## 총 시간 5분 + 모든 프로젝트 git clone 시간 (별도). ## 다음 - [맥미니 ↔ 맥북 일상 동기화](/examples/multi-device) - [.env 보안 패턴](/examples/env-sync) - [Claude Code 자연어 사용](/examples/claude-code) --- # Telegram 으로 외부에서 태스크 추가 집에서 출근길 — 지하철에서 빠르게 메모/태스크. Telegram 봇이 가장 빠름. ## 셋업 [Telegram 연결](/mcp/telegram) 페이지의 1회 셋업 완료 후. ## 패턴 1: 즉시 태스크 지하철에서 갑자기 떠오른 거: ``` /task 오늘 저녁 7시 헬스장 ``` 봇 응답: ``` ✅ "헬스장" 추가 📅 오늘 19:00 (5시간 후) 🏷️ 운동 🔔 30분 전 알림 ``` ## 패턴 2: 음성 → 태스크 (계획됨) 음성 메시지 → Whisper STT → `create_task`: ``` [🎤 voice] "내일 오전 10시 강남역 미팅, 30분 전 알려줘" ``` 봇: ``` ✅ 강남역 미팅 (내일 10:00, 알림 30분 전) ``` ## 패턴 3: 빠른 메모 생각난 아이디어: ``` /memo Rails 8 의 Solid Queue 가 PUMA 안에서 돌릴 수 있다 (SOLID_QUEUE_IN_PUMA=true) ``` 봇: ``` 📝 메모리 저장됨 — type: feedback "Rails 8 의 Solid Queue 가 PUMA 안에서 돌릴 수 있다..." ``` 다음번 Claude Code 세션에서 자동으로 이 메모리 사용. ## 패턴 4: 오늘 할 일 확인 지하철에서 오늘 일정 다시 보기: ``` /today ``` 봇: ``` 오늘 할 일 (3개): ⭐ 강남역 미팅 10:00 회의 준비 09:30 헬스장 19:00 [모두 보기] [내일] ``` ## 패턴 5: 인라인 검색 다른 채팅에서 ainote 검색 결과 공유: ``` @clawdbot Stripe naming ``` → 검색 결과 (본인만 보임) → 클릭하면 친구 채팅에 인용. ## 패턴 6: 알림 → 즉시 처리 ainote 알림이 봇 DM 으로: ``` ⏰ 30분 후 마감 "강남역 미팅" — 오늘 10:00 [완료] [+30분 미루기] [취소] ``` 버튼 한 번으로 처리. Claude/웹 안 열어도 됨. ## 패턴 7: 멀티-단계 워크플로우 ``` /ai 다음주 마감 중요한 거 모아서 일정표로 만들어줘 ``` 봇 (내부 Anthropic API): 1. `list_tasks {due_date_start: "2026-05-18", due_date_end: "2026-05-24", is_important: true}` 2. 결과 정리 3. 표/요약 응답 ``` 다음주 중요한 일정: 월요일 ⭐ 임원 미팅 09:00 ⭐ 디자인 리뷰 10:00 수요일 ⭐ 디자인 검수 14:00 금요일 ⭐ 분기 회고 15:00 🔧 주말 출근 가능성: 디자인 검수 결과에 따라 ``` ## 한계 | | Telegram | Claude Code | |---|---|---| | 속도 | ⚡ 즉시 (모바일) | 💻 데스크톱 필요 | | 도구 갯수 | 6-8개 (자주 쓰는 거) | 17개 전체 | | 자동화 | △ (사용자 명령마다) | ✅ AI 자율 | | 컨텍스트 | 단일 메시지 | 긴 대화 | → Telegram = **모바일 빠른 입력**, Claude Code = **깊은 작업**. ## 보안 - 봇 → ainote 호출은 사용자별 MCP key 사용 (연동 시 발급) - DM 메시지 본문 ainote 에 저장 안 함 (`/memo` 명시 시만) - 그룹 채팅: 메시지 자동 학습 X - 봇 차단 시 즉시 호출 중단 ## 다음 - [Telegram 연결 셋업](/mcp/telegram) - [태스크 개요](/tasks/overview) - [Claude Code 자연어 사용](/examples/claude-code) --- # API Key 인증 ainote 는 **두 종류**의 키를 지원합니다. | 키 종류 | 길이 | 용도 | 발급 위치 | |--------|------|------|----------| | **User API Key** | 24자 | 개인 앱/스크립트 | 웹 설정 → API | | **MCP Key** | 64자 | MCP 클라이언트 | `signup_and_get_key` 또는 웹 설정 | 대부분의 사용자는 **MCP Key** 만 쓰면 됩니다. ## MCP Key 발급 — 3가지 방법 ### 방법 A. Claude 에서 가입 (가장 쉬움) MCP 등록 후 (헤더 없이도 가능): ``` ainote 가입 시켜줘 — 이메일 X / 비번 Y ``` → `signup_and_get_key` 호출 → 키 반환. ### 방법 B. 기존 계정 로그인 ``` ainote 로그인 — 이메일 X / 비번 Y, 키 발급 ``` → `login_and_get_key` 호출. ### 방법 C. 웹에서 직접 → "새 MCP 키 발급" → 라벨 입력 → 복사. ## 헤더 형식 JSON-RPC 호출 시: ```http POST /api/mcp HTTP/1.1 Host: api.ainote.dev Content-Type: application/json Authorization: McpKey h7Axq9XPsDTD2qr5yqtcCSaQ... ``` ::: tip Bearer 도 호환 일부 MCP 클라이언트는 `Authorization: Bearer ...` 만 지원합니다. ainote 는 `Bearer` 와 `McpKey` 두 prefix 모두 받습니다. ::: ## 키 권한 모델 키 발급 시 권한 선택: - **read** — `list_*`, `get_*`, `pull_*` 만 호출 가능 - **read_write** (기본) — 모두 가능 - **admin** — 다른 키 관리 + 사용량 통계 조회 기본적으로 **per-key 사용량 통계**가 자동 기록됩니다 (요청수, 토큰수, 마지막 호출 시각). 웹 설정에서 확인. ## 키 회전 / 폐기 ``` ainote 키 폐기 — 이름 "old-laptop" ``` → `revoke_mcp_key` (계획됨, 현재는 웹에서만). ## CLI 환경변수 shell 함수로 쓸 때: ```bash # ~/.zshrc export AINOTE_API_URL="https://api.ainote.dev" # host only — shell 함수가 /api/mcp 를 append export AINOTE_API_KEY="h7Axq9XPsDTD2qr5yqtcCSaQ..." ``` 자세한 패턴: [CLI / shell 함수](/cli/shell-function). ## 보안 권장 - ✅ 키는 `~/.claude.json` 또는 환경변수로만. **절대 git 에 커밋 X**. - ✅ 노트북마다 별도 키 발급 (라벨로 구분: `macmini-2026-04`). - ✅ 분실/노출 의심 시 즉시 폐기. - ❌ 한 키를 여러 사용자가 공유하지 마세요 — 사용량 통계 의미 사라짐. ## 다음 - [JSON-RPC 호출 형식](/reference/json-rpc) - [에러 코드](/reference/errors) - [MCP 등록 (Claude Code)](/mcp/claude-code) --- # 데이터 내보내기 / 삭제 ainote 의 모든 데이터는 **너의 것**. 언제든 가져갈 수 있고 삭제할 수 있습니다. ## 내보내기 — 전체 백업 ### 방법 A. Web UI → "전체 내보내기" 버튼. 이메일로 ZIP 링크 (보통 5분 이내): ``` ainote-export-2026-05-07.zip ├── tasks.json # 모든 태스크 ├── categories.json ├── dev_docs/ # 모든 dev_docs (마크다운) ├── memory/ # 토픽 메모리 ├── vaults/ # 각 vault 의 git bundle │ ├── personal.bundle │ └── work.bundle ├── notifications.json └── manifest.json # 메타데이터 ``` ### 방법 B. MCP 도구 (계획) ``` ainote 전체 내보내기 — 형식: zip ``` → `export_all` (계획됨, v0.5). 현재는 개별 도구로: ``` list_tasks → tasks.json list_dev_docs → dev_docs.json (전부) vault_list → 각 vault 마다 vault_sync 후 로컬 디렉토리 백업 ``` ### 방법 C. 직접 git pull 각 vault 는 git 저장소이므로: ```bash git clone https://api.ainote.dev/vaults/.git ``` (인증: HTTPS basic auth, username = email, password = MCP key) ## 부분 삭제 ### 단일 태스크 ``` ainote 에서 태스크 #1234 삭제해줘 ``` → soft-delete (30일 후 영구). 그 사이 복구 가능. ### 단일 메모리 ``` ainote 에서 dev_doc "old-notes.md" 삭제해줘 ``` → `delete_dev_doc` 즉시 영구 삭제. ### Vault 전체 ``` ainote 에서 vault "experiments" 삭제해줘 ``` → Web UI 에서만 가능 (실수 방지). ## 계정 완전 삭제 ### 절차 1. 접속 2. "계정 삭제" 클릭 3. 비밀번호 재입력 + "DELETE" 타이핑 4. 이메일 확인 링크 클릭 ### 삭제 타임라인 | 시점 | 무엇 | |------|------| | 즉시 | API 접근 차단, 모든 키 무효화 | | 즉시 | 다른 디바이스 push 알림 중단 | | **30일** | 모든 데이터 영구 삭제 (PostgreSQL + git + S3) | | 30일 이내 | 로그인 가능 → 복구 가능 | ### 30일 이전 복구 `me@example.com` 으로 로그인 → "계정 복구" 버튼. ### 30일 이후 복구 불가능. 백업도 30일 이후 모두 폐기. ## GDPR / 개인정보 ainote 는 한국 개인정보보호법 + GDPR 준수: - **데이터 최소화**: 이메일 + 비번 hash 외 강제 수집 X - **포터빌리티**: 위 export 절차로 모두 받아갈 수 있음 - **삭제권**: 30일 내 완전 삭제 보장 - **고지**: 보관/암호화/제3자 공유 모두 [개인정보처리방침](https://ainote.dev/privacy) 명시 ## 셀프호스팅 시 데이터가 너의 서버에 있으므로: - PostgreSQL `pg_dump` 로 직접 백업 - vault git repo 직접 clone - 삭제: 인스턴스 종료 + DB drop ## 다음 - [Troubleshooting](/guide/troubleshooting) - [개인정보처리방침](https://ainote.dev/privacy) (외부 링크) --- # FAQ ## 일반 ### ainote 와 Notion / Obsidian 차이가 뭔가요? | | ainote | Notion | Obsidian | |---|---|---|---| | **AI 직접 호출** | ✅ MCP 50+개 도구 | ❌ | △ 플러그인 | | **자연어 태스크** | ✅ | △ AI Add | ❌ | | **Git backend** | ✅ | ❌ | △ Sync 플러그인 | | **셀프호스팅** | ✅ | ❌ | ✅ | | **다중 디바이스 sync** | ✅ HLC | ✅ 클라우드 | △ Sync 유료 | ainote 는 **AI 가 1급 시민**이라는 점이 가장 다릅니다. Notion/Obsidian 은 AI 통합이 추가 기능이지만 ainote 는 시작부터 MCP 우선. ### 무료인가요? 현재 v0.x 는 전부 무료. 유료 플랜은 v1 이후 (저장량/MCP 호출 무제한 등). ### 데이터 어디 저장되나요? 호스팅 (api.ainote.dev): Render Singapore 리전, PostgreSQL + git 저장소. 셀프호스팅: 자체 인스턴스 가능 (도커 이미지 계획). ### 데이터 내보낼 수 있나요? [데이터 내보내기 / 삭제](/guide/data-export) 참고. 모든 메모리/태스크/vault 를 ZIP 또는 git push 로 내보낼 수 있고, 계정 삭제 시 30일 안에 영구 삭제. ## 기능 ### Obsidian 사용 중인데 마이그레이션 가능? 네. `vault_clone` 으로 기존 Obsidian vault 를 git 으로 등록하면 됩니다. `[[wikilinks]]` 그대로 호환. ``` ainote 에 ~/Documents/MyObsidianVault 를 vault_clone 해줘 ``` ### CLAUDE.md 17개 프로젝트 동기화 어떻게? [CLAUDE.md 통합 관리](/memory/claude-md) 참고. `create_dev_doc` 으로 한 번 등록 → 다른 기기에서 `pull_dev_docs` 한 번이면 전체 복원. ### 태스크 알림 어디로 오나요? 설정한 채널 모두: - iOS / Android 푸시 (앱 설치 시) - Telegram (봇 연동 시) - Web Push (PWA 등록 시) - Apple Watch (iOS 동기화) ### 반복 태스크 지원? 네. `create_task` 의 `repeat_rule` 파라미터: ```json { "content": "주간 회고", "repeat_rule": "weekly" } ``` `"daily"`, `"weekly"`, `"monthly"` 또는 iCalendar RRULE 문자열 지원 (서버 버전에 따라). ## MCP / AI 통합 ### Claude / GPT 가 내 모든 노트 다 보나요? 아니요. **MCP 도구를 호출할 때만** 그 도구가 반환하는 데이터만 봅니다. 예시: - `list_tasks` → 필터에 매칭되는 태스크만 - `get_dev_doc` → 그 한 문서만 - `vault_sync` → 변경된 파일 목록 (내용 X) 도구 호출 단위로 Claude Code 에서 승인 가능. ### ChatGPT 에서도 되나요? 네. SSE bridge (`ainote-mcp-http`) 로컬 실행 → ChatGPT MCP connector 등록. [ChatGPT 연결](/mcp/chatgpt) 참고. ### Telegram 에서 외부에서 쓸 수 있나요? 네. 봇 연동 후 `/task 내일 회의` 같은 식으로. [Telegram 연결](/mcp/telegram). ## 가격 / 제한 ### Rate limit 걸렸어요 | 키 종류 | 분당 | 일당 | |--------|------|------| | User API Key | 60 | 10,000 | | MCP Key (free) | 120 | 20,000 | 429 응답 시 `Retry-After` 헤더 보고 대기. ### 저장 용량 제한? v0.x: 사용자당 100 MB (메모리 + dev_docs). Vault 는 별도 git 저장소 1 GB까지. ## 기술적 ### Rails 8 라는데 React Native 앱은? `ainote_server/` 가 Rails 8 (API + Inertia.js + Svelte 5 웹). 모바일은 별도: - iOS/Android: Flutter (BLoC + Clean Architecture) - macOS: 네이티브 SwiftUI - Apple Watch: WatchKit - Chrome/Safari: 확장 프로그램 ### 오픈소스인가요? 코어는 MIT. . ## 안 풀리면 - [Troubleshooting](/guide/troubleshooting) - GitHub Issues: - Email: support@ainote.dev --- # 5분 Quickstart 계정 없이 바로 시작. Claude Code 기준 (다른 클라이언트는 [MCP 섹션](/mcp/overview) 참고). ## 1. MCP 서버 등록 (1분) `~/.claude.json` 에 추가: ```json { "mcpServers": { "ainote": { "type": "http", "url": "https://api.ainote.dev/api/mcp" } } } ``` ::: warning `type: "http"` 필수 원격 MCP 서버는 반드시 `type` 명시. 빠지면 `~/.claude.json` 전체 mcpServers 블록이 스키마 검증 실패해서 모든 user-level MCP 가 미로드됩니다. ::: Claude Code 재시작. ## 2. 가입 (1분) Claude 에서: ``` ainote 가입 시켜줘 — 이메일 you@example.com / 비번 안전한패스워드123 ``` Claude 가 `signup_and_get_key` 도구를 호출합니다. 응답에 API 키가 옵니다 (64자): ``` Mcp Key: h7Axq9XPsDTD2qr5yqtcCSaQ... ``` ## 3. API Key 등록 (30초) `~/.claude.json` 에 헤더 추가: ```json { "mcpServers": { "ainote": { "type": "http", "url": "https://api.ainote.dev/api/mcp", "headers": { "Authorization": "McpKey h7Axq9XPsDTD2qr5yqtcCSaQ..." } } } } ``` Claude Code 재시작. ## 4. 첫 태스크 (30초) ``` 내일 오전 10시 회의 준비 추가해줘 ``` Claude 가 `create_task` 호출: ```json { "content": "회의 준비", "due_date": "2026-05-08T10:00:00+09:00", "is_important": false } ``` ## 5. 첫 메모리 저장 (1분) ``` 이거 메모리에 저장해줘: - 우리 회사는 Stripe 방식 따라서 API 응답 snake_case 유지함 - 절대 camelCase 로 변환하지 말 것 ``` Claude 가 `create_dev_doc` 호출: ```json { "title": "api-naming-convention.md", "category": "feedback", "content": "# API Naming\n\n우리는 Stripe 방식으로 snake_case..." } ``` ## 6. 다른 기기에서 가져오기 (1분) 다른 기기 Claude: ``` ainote 에서 내 메모리 다 가져와 ``` `pull_dev_docs` 가 모든 메모리를 `local_path` 기준으로 복원합니다. ## 다음 단계 | 하고 싶은 것 | 어디로 | |------------|--------| | 태스크 18+ 필터 | [태스크 필터링](/tasks/filtering) | | CLAUDE.md 동기화 | [메모리 / CLAUDE.md](/memory/claude-md) | | Obsidian 마이그 | [Vault 시작](/vault/overview) | | 다중 기기 sync | [동기화 개요](/sync/overview) | | ChatGPT 에서 쓰기 | [ChatGPT 연결](/mcp/chatgpt) | | Telegram 봇 | [Telegram 연결](/mcp/telegram) | ## 안 되면 - [Troubleshooting](/guide/troubleshooting) - GitHub Issues: --- # 계정 만들기 (MCP 안에서) ainote 의 가장 흥미로운 기능 — **웹사이트 방문 없이 Claude/Cursor 안에서 바로 가입**. ## 작동 원리 `signup_and_get_key` 와 `login_and_get_key` 두 도구는 **인증 헤더 없이** 호출 가능. 다른 모든 도구는 키 필수. ``` [키 없음] → signup_and_get_key → [키 발급] → 나머지 16개 도구 ``` ## 1단계: MCP 등록 (키 없이) `~/.claude.json`: ```json { "mcpServers": { "ainote": { "type": "http", "url": "https://api.ainote.dev/api/mcp" } } } ``` `headers` 항목 자체를 빼면 됩니다. ## 2단계: Claude 에서 가입 요청 ``` ainote 가입 시켜줘 — 이메일 me@example.com / 비밀번호 SuperSecret123 ``` Claude 가 호출: ```json { "method": "tools/call", "params": { "name": "signup_and_get_key", "arguments": { "email": "me@example.com", "password": "SuperSecret123", "name": "Seunghan" } } } ``` 응답: ``` ✅ 가입 완료 이메일: me@example.com MCP Key: h7Axq9XPsDTD2qr5yqtcCSaQwertyuiopASDFghjkLZXCVbnm1234567890QWERTY ⚠️ 이 키는 다시 표시되지 않습니다. ~/.claude.json 에 저장하세요. ``` ## 3단계: 키 등록 ```json { "mcpServers": { "ainote": { "type": "http", "url": "https://api.ainote.dev/api/mcp", "headers": { "Authorization": "McpKey h7Axq9XPsDTD2qr5yqtcCSaQ..." } } } } ``` Claude Code 재시작. ## 기존 계정 로그인 이미 ainote 계정이 있다면: ``` ainote 로그인 — 이메일 me@example.com / 비번 SuperSecret123 ``` → `login_and_get_key` 호출, 새 키 발급 (기존 키는 그대로 유지). ## 비밀번호 규칙 - 최소 6자 - 영문 + 숫자 포함 권장 - 특수문자는 선택 (대부분 쉘 escape 문제 회피용) ## 보안 - 가입 호출은 **HTTPS only** (TLS 1.2+) - 비번은 bcrypt(cost=12) 저장 - 키는 SHA-256 해시 + 마지막 4자만 표시 - 가입 IP/User-Agent 자동 기록 ## 자주 하는 실수 | 증상 | 원인 | |------|------| | `email already taken` | 이미 가입된 이메일 → 로그인 사용 | | `invalid email format` | `@` 없거나 도메인 누락 | | `password too short` | 6자 미만 | | `tool not found` | MCP 등록 안 됨 또는 reload 안 함 | ## 다음 - [API Key 인증 자세히](/guide/auth) - [`signup_and_get_key` API](/reference/signup) - [Claude Code 연결](/mcp/claude-code) --- # Troubleshooting ## MCP 연결 ### 도구 목록에 ainote 가 안 나옴 증상: Claude Code `/mcp` 에 `ainote` 보이지 않음. 원인 + 해결: 1. **`type` 필드 누락** (가장 흔함) ```json { "type": "http", "url": "..." } ``` `type` 빠지면 mcpServers 블록 전체가 schema 검증 실패 → 다른 MCP 도 같이 안 뜸. 2. **JSON 문법 오류** — `~/.claude.json` 을 `jq .` 로 검증 ```bash jq . ~/.claude.json > /dev/null ``` 3. **Claude Code 재시작 필요** — 설정 변경 후 reload 필수. ### 401 Unauthorized ```json { "error": { "code": -32001, "message": "Unauthorized" } } ``` 원인: - `Authorization` 헤더 누락 - 키 prefix 잘못 (`McpKey ` 접두사 + 공백 1개) - 키 만료/폐기됨 테스트: ```bash curl -X POST https://api.ainote.dev/api/mcp \ -H "Authorization: McpKey YOUR_KEY" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` ### 응답 끊김 / Cold Start Render free tier 는 15분 idle 시 sleep. 첫 호출 ~5초 wait. 해결: 유료 플랜 또는 cron 으로 1분마다 ping. ## 태스크 ### 자연어 파싱이 이상해요 "내일 오후 3시" 가 다음날 오후 3시로 안 잡힐 때: - 시간대 확인 — `Asia/Seoul` 기본, 사용자 설정에서 변경 가능 - 명시적으로 ISO 8601 사용: `2026-05-08T15:00:00+09:00` ### 알림이 안 와요 체크리스트: 1. iOS/Android 앱 알림 권한 ON 2. Telegram 봇 연동 완료 (설정 → Telegram) 3. Web Push: 브라우저에서 ainote.dev 알림 허용 4. `reminder_minutes` 설정됨? (기본 알림 없음) 5. Solid Queue 정상 동작 — `bin/jobs status` (셀프호스팅) ## 메모리 / Dev Docs ### `pull_dev_docs` 가 파일 만들지 않음 원인: 등록할 때 `local_path` 안 넣었음. 확인: ``` ainote 에서 "tennis-bracket-claude.md" 의 local_path 보여줘 ``` 빈값이면: ``` local_path 를 "/Users/seunghan/tennis_bracket/CLAUDE.md" 로 업데이트해줘 ``` ### CLAUDE.md 동기화 충돌 두 기기에서 동시 편집 → 마지막 write 가 이김 (LWW). 복구: ```bash # 양쪽 버전 확인 git log -p --all -- "global/claude/global-claude-guidelines.md" # 손실된 버전 복원 git show :global/claude/global-claude-guidelines.md > /tmp/lost.md ``` ## Vault ### `vault_sync` 가 conflict 표시 Git 3-way merge conflict. 수동 해결: ```bash cd ~/ainote-vaults/ git status # conflict 파일 확인 # editor 로 conflict marker 해결 git add . git commit ainote vault_sync # 다시 시도 ``` ### Obsidian 에서 안 보임 체크: - `.obsidian/` 폴더 vault root 에 있나? - Obsidian 에서 `File → Open Vault as Folder` 로 열기 - ainote vault 의 git working tree 가 Obsidian vault root ## 동기화 (Sync) ### 새 기기에서 state.json 없음 ```bash ainote sync_pull --initial ``` `--initial` 플래그가 첫 동기화 모드 — manifest 의 모든 sync 항목을 받아옴. ### 충돌 디렉토리에 파일 쌓임 `~/.claude/ainote-sync/conflicts/` 에 파일들: ```bash # 최근 conflict ls -lt ~/.claude/ainote-sync/conflicts/ | head # 수동 해결: diff 보고 한쪽 선택 diff conflicts/path__local.md conflicts/path__remote.md ``` 자세히: [sync 충돌 해결](/sync/conflicts). ## 빌드 / 개발 ### `npm run dev` 가 안 됨 ```bash cd ~/ainote/docs-site rm -rf node_modules .vitepress/cache npm install npm run dev ``` Node 20+ 필요. ### llms.txt 빌드 실패 ```bash node scripts/build-llms.mjs ``` 직접 실행해서 오류 확인. `public/` 폴더 권한 문제일 수 있음. ## 더 안 풀리면 - GitHub Issues: - Email: support@ainote.dev - 디버그 정보 첨부: 클라이언트 (Claude Code 버전), `~/.claude.json` (키 마스킹), 에러 응답 전체 --- # ainote 란? > 한 줄: **AI 가 직접 쓰는 노트 앱.** 태스크 + 메모리 + Dev Docs 를 MCP 도구로 노출해서 Claude/Cursor/ChatGPT 가 자연어로 조작합니다. ## 현재 v1.x — 2가지 핵심 기능 ### 1. 태스크 (Tasks) "내일 오전 10시 회의 준비" 한 줄로 마감/위치/카테고리/중요도/반복 모두 자동 추출. - Solid Queue 기반 알림 (5분 단위 reminder, 마감 임박 푸시) - 18개 필터 옵션 (오늘 / 위치 / 카테고리 / 마감 범위) - MCP 도구 5개: [`create_task`](/reference/create-task), [`update_task`](/reference/update-task), [`delete_task`](/reference/delete-task), [`list_tasks`](/reference/list-tasks), [`list_categories`](/reference/list-categories) ### 2. 메모리 / Dev Docs **4가지 타입** (frontmatter 분류) 의 마크다운 메모리: - `feedback` — "이렇게 하지 마" 류 가이드 - `project` — 진행중 프로젝트 컨텍스트 - `reference` — 외부 시스템 포인터 (Slack 채널, Linear 프로젝트, Grafana URL) - `user` — 사용자 자체 정보 (역할, 선호, 도메인 지식) 추가로 **Dev Docs** — CLAUDE.md / Cursor rules / Windsurf rules / Copilot instructions 를 한 곳에서 중앙 관리. MCP 도구 7개 (`create_dev_doc`, `update_dev_doc`, `pull_dev_docs` 등). ## 향후 — 설계 완료, 구현 대기 ### Vault (Obsidian-style) git 백엔드 마크다운 저장소 — `[[wikilinks]]` 호환, 로컬 Obsidian 양방향 sync. **도구 5종 설계 완료**, 구현 대기. ### 다중 디바이스 동기화 HLC (Hybrid Logical Clock) + Star Topology + LWW. **도구 3종 + 클라이언트 알고리즘 설계 완료**, 구현 대기. 자세한 설계: [vault/overview](/vault/overview), [sync/overview](/sync/overview). ## 누가 왜 쓰는가 | 사용자 | 용도 | |--------|------| | **AI 헤비유저** | Claude 안에서 "내 노트에 X 추가" 자연어로 | | **멀티디바이스 개발자** | 17개 프로젝트 CLAUDE.md 동기화 (맥미니 ↔ 맥북) | | **Obsidian 사용자** | git-backed vault 로 마이그레이션, MCP 로 AI 접근 추가 | | **태스크 매니저** | Things/Todoist 대신 "AI 가 직접 추가하는" 할 일 | | **외부 봇 사용자** | Telegram 으로 모바일 외부에서 빠르게 메모/태스크 | ## 다른 도구와 비교 | | ainote | Notion | Obsidian | Things | |---|---|---|---|---| | AI MCP 도구 | ✅ 15개 (+8 설계) | ❌ | △ 플러그인 | ❌ | | 자연어 태스크 | ✅ | △ | ❌ | ❌ | | 다중 디바이스 sync | ✅ HLC | ✅ 클라우드 | △ Sync 유료 | ✅ iCloud | | 셀프 호스팅 | ✅ | ❌ | ✅ | ❌ | | Git backend | ✅ | ❌ | △ 플러그인 | ❌ | | 무료 티어 | ✅ | △ | ✅ | ❌ | | 가격 (유료) | $0–TBD | $10/mo | $5/mo | $50 1회 | ## 작동 방식 (30초) ``` ┌─────────────┐ MCP (JSON-RPC) ┌──────────────────┐ │ Claude Code │ ◄────────────────────────────► │ ainote API │ │ Cursor │ │ Render/Singapore │ │ ChatGPT │ create_task, sync_push, │ │ │ Telegram │ vault_clone, ... │ PostgreSQL + Git │ └─────────────┘ └──────────────────┘ ▲ │ │ 자연어 │ 같은 데이터 ▼ ▼ 사용자 iOS / Android / Web / macOS ``` 너가 "내일 오전 10시 회의 준비" 라고 Claude 에 말하면: 1. Claude 가 MCP 도구 `create_task` 호출 2. ainote 가 자연어 → `{content, due_date, is_important}` 파싱 3. PostgreSQL 저장 + Solid Queue 알림 스케줄 4. 모든 디바이스에 동기화 (iOS 앱 푸시 도착) ## 다음 단계 - 일단 써보자 → [**5분 Quickstart**](/guide/quickstart) - 어떻게 작동하나 → [**MCP 란?**](/mcp/overview) - 태스크 도구 자세히 → [**태스크 개요**](/tasks/overview) - 메모리 모델 자세히 → [**메모리 4가지 타입**](/memory/types) --- # Session Handoff 세션 간 작업 인수인계 도구. 컨텍스트가 차서 다른 세션·디바이스에서 이어가야 할 때, **자기완결 텍스트 한 장**으로 작업 환경과 다음 STEP을 넘긴다. ## 왜? 긴 작업이 한 세션에서 안 끝나거나 컨텍스트가 차서 새 세션을 열 때, 다음 세션의 Claude(또는 본인)가 **5분 안에 환경 복구 + 작업 재개**할 수 있어야 한다. 핸드오프는 이를 위한 표준 텍스트 메모. 특징: - **멀티 디바이스 동기화** — 맥미니에서 저장한 핸드오프를 맥북·iPhone Claude Code 세션에서 즉시 회수 - **7일 자동 purge** — 서버 측 TTL. 영구 보존 필요 시 메모리 또는 dev doc 으로 이전 - **시각 단위 다중 저장** — 같은 날 같은 토픽 여러 번 저장 가능 (HHMM 접미사) ## 도구 | Tool | 용도 | |---|---| | `handoff_save` | 핸드오프 생성/덮어쓰기 | | `handoff_get` | 단일 핸드오프 본문 회수 | | `handoff_list` | 최근 핸드오프 목록 (옵션: 프로젝트 필터) | ⚠️ `handoff_list` / `handoff_get` 호출 시 7일 지난 핸드오프 자동 purge 가 같이 실행됨 (side effect). 자동화 에이전트는 destructive 한 maintenance call 로 취급. ## 저장 (Save) ```js mcp__ainote__handoff_save({ project: "sample-app", // 영문 소문자 + 숫자 + 하이픈만 topic: "login-flow", content: "<자기완결 본문>", date: "2026-05-13", // optional, 생략 시 서버 today time: "1555" // optional, HHMM (KST). 같은 날 다중 저장 시 접미사 }) ``` 저장 경로: `handoffs/{project}-{topic}-[{HHMM}-]{YYYY-MM-DD}.txt` 작성 원칙: 1. **자기완결**: 다음 세션은 이전 대화 메시지를 못 봄. 파일 하나만으로 환경 복구 2. **사실만**: "잘 동작" 대신 "BUILD SUCCESSFUL" 같은 검증 가능한 표현 3. **절대 경로 + 줄 번호** 명시 → 다음 세션이 `grep`/`Read` 즉시 가능 4. **다음 STEP 은 명령 단위** ("테스트 작성" X → "`/path/X.kt` 작성, 케이스 A/B/C" O) 5. **알려진 함정** 필수 — 이번 세션에서 부딪힌 빌드/타입/import 함정은 재발 가능성 큼 6. **비밀값 평문 금지** — "ENV 의 X (1Password 'sample-app' 항목)" 식으로 참조만 ## Frontmatter (v2) `content` 본문 맨 앞에 YAML frontmatter 를 두면, 서버가 저장 시 자동 파싱해 `file_indices.frontmatter` 에 인덱싱한다. `handoff_list` 는 이 필드로 **본문을 받지 않고** 서버사이드 필터링을 한다. frontmatter 없는 v1 평문 핸드오프도 그대로 동작한다 (필터 지정 시에만 제외됨). ```yaml --- project: sample-app # 슬러그 (영문 소문자+숫자+하이픈) topic: login-flow title: "로그인 플로우 포팅" # 사람·LLM 식별용, 한국어 OK session_date: "2026-05-23" status: in_progress # in_progress | paused | completed | blocked has_blockers: false task_type: feature # feature | bugfix | refactor | research | ops tags: [auth, oauth, mobile] --- # SUMMARY ... ``` **서버가 필터링하는 필드** (`handoff_list` 인자): | 필드 | 필터 | |---|---| | `status` | `status: "in_progress"` 등 — v1(frontmatter 없음) 은 필터 지정 시 제외 | | `has_blockers` | `true` / `false` | | `task_type` | `feature` / `bugfix` / `refactor` / `research` / `ops` | | `tags` | 배열 AND 매치 — 준 태그를 **모두** 가진 항목만 (예: `['auth','mobile']`) | 필터 외에도 `handoff_list` 응답 각 항목에는 `title`·`priority`·`session_time`· `handoff_version` 등이 본문 없이 실려 온다 (목록 훑기용). 필터 대상은 위 4개다. ### 참조 자동첨부 (클라이언트) Claude Code `session-handoff` 스킬은 저장 직전 본문에서 plan/memory/skill 파일 참조를 감지해 vault 에 함께 push 하고, frontmatter 에 `{vault_path, git_sha, local_path}` 로 기록한다 — 다음 세션이 그 파일들을 sha 대조로 복원. 이 자동첨부는 **클라이언트(스킬) 동작**이며 서버 도구가 강제하는 것은 아니다. ```yaml referenced_plans: - vault_path: global/planning/{proj}/{plan}.md git_sha: b0959e68 local_path: ~/{proj}/.planning/{plan}.md referenced_memory: [ ... ] # 동일 형식 referenced_skills: [ ... ] # 동일 형식 ``` ## Environment sidecar (핸드오프 환경 분리) 프로젝트의 **안정 환경 사실**(테스트 계정 참조, 에뮬레이터/시뮬레이터 셋업, 빌드 커맨드, 반복 함정)을 핸드오프마다 다시 쓰지 않고 vault 의 한 파일로 분리한다. 핸드오프의 `ENVIRONMENT` 섹션은 "이번 세션 delta + 치명 정보 1줄" 로 줄고, 안정 사실은 sidecar 가 정본이 된다. - **정본 위치**: `global/planning/environments/{proj}.md` - ⚠️ `handoffs/` 아래에 두지 말 것 — 핸드오프 TTL(7일) purge 가 `handoffs/` 전체를 재귀로 쓸어가므로 sidecar 도 소멸한다. `global/planning/` 은 TTL 이 없다. - **핸드오프에서 참조**: frontmatter 에 sha 를 박아 버전을 고정한다. ```yaml environment_ref: vault_path: global/planning/environments/{proj}.md git_sha: ``` ⚠️ 여기서 `git_sha` 는 vault 가 저장하는 **content SHA1** (`Digest::SHA1.hexdigest(content)`) 이지 git blob-object hash 가 아니다. 클라이언트는 같은 계산(`sha1(content)`)으로 네트워크 없이 얻을 수 있다. - **resolution 은 클라이언트가 수행**: `/r` 복구(또는 handoff CLI)가 위 ref 를 읽어 sidecar 를 가져온다. 로컬 캐시의 content SHA1 이 `git_sha` 와 일치하면 네트워크 0회로 캐시를 쓰고, 다르면 그 파일만 한 번 pull 해서 갱신한다. 가져오기에 실패하거나 오프라인이면 **환경 정보만 degrade** 되고 핸드오프 본문 복구는 절대 막히지 않는다. (서버는 sidecar 를 일반 vault 파일로 저장·인덱싱할 뿐, `environment_ref` 를 해석하지는 않는다.) - **자기완결 유지**: sidecar 를 못 받는 상황을 대비해 핸드오프 본문에 **치명 정보 1줄 요약은 항상 남긴다**. ## 복구 (Restore) — 시간대 cluster 가져오기 여러 프로젝트를 같은 시간대에 몰아서 저장한 경우 (예: 오전에 5개), 하나만 회수하면 **인접 핸드오프를 놓칠 위험**이 있다. 복구 모드는 최근 cluster + 직전 cluster 의 메타데이터를 묶어 보여줘서 빠뜨림 없이 작업을 재개하도록 돕는다. ### 알고리즘 1. `handoff_list({limit: 50})` 호출 (응답은 `updated_at` 내림차순) 2. **Cluster 탐지 (gap 기반)**: - 기본 `GAP_THRESHOLD = 30분` - 인접 항목 간 gap 계산. `gap ≤ 30분` → 같은 cluster, 초과 시 break 3. **반환 범위**: 최신 cluster + 직전 cluster 1개 (빠진 것 확인용) 4. **출력**: cluster 별 표 (HHMM KST / project / topic) — 본문은 fetch 하지 않음 5. 사용자가 선택 시 단일 메시지에서 **병렬** `handoff_get` 호출 ### 출력 예시 ``` ## Cluster 1 — 2026-05-19 08:18~08:36 KST (5건, 18분 span) | 시각 | project / topic | |---|---| | 08:36 | tennis / bracket-share-button-universal-links-0830 | | 08:35 | refra / v2-deploy-step1-done-0834 | | 08:30 | krx / listing-phase-10-complete-0830 | | 08:24 | krx / listing-testflight-build2-uploaded-0824 | | 08:18 | tennis / bracket-predict-feature-0818 | ## Cluster 2 (직전) — 2026-05-19 00:16~00:30 KST (2건) | 00:30 | ainote / web-logout-cookie-fix-0030 | | 00:16 | tennis / bracket-predict-feature-0016 | ``` ### 옵션 - "오전만"/"오후만" → KST hour 필터 (06~12 / 12~18) - "프로젝트 X 만" → project/topic 필터 후 cluster - "이전 cluster 더" → 직전 1개 → 2~3개로 확장 - "본문 전부 펴줘" → cluster 내 모든 항목 병렬 fetch ### 구현 별도 MCP tool 없이 기존 `handoff_list` + 클라이언트의 in-conversation cluster 추론으로 처리. Claude Code 사용자는 `session-handoff` 스킬의 §복구 섹션 알고리즘 자동 발동. ## Claude Code 트리거 예시 저장: - "이어서 다른 세션", "핸드오프 만들어", "다음 세션 넘기기" - "바탕화면에 핸드오프 만들어" (Desktop fallback) 복구: - "오전 핸드오프 복구해줘", "최근 핸드오프 복구" - "이어서 작업 가져와", "오늘 작업한 거 묶어서 보여줘" ## 안티패턴 - ❌ "잘 진행됨" 같은 측정 불가 표현 - ❌ 절대 경로 없이 "feature/auth 의 XXX" — 다음 세션이 못 찾음 - ❌ STEP 에 "테스트 추가" 같은 추상적 한 줄 - ❌ 비밀값 평문 포함 - ❌ project/topic 슬러그에 한글·이모지·공백 (slugify 가 비-ASCII 를 `-` 로 변환) - ❌ 복구 시 메타데이터 없이 곧바로 모든 본문 fetch (토큰 낭비) - ❌ cluster 경계 무시하고 임의로 N개 자름 --- ## 30초 정리 ainote 는 **AI 가 직접 호출하는** 노트·태스크·메모리·다중 디바이스 동기화 백엔드다. 같은 50+개 도구를 **두 가지 프로토콜로 동시 노출**한다: - **MCP JSON-RPC** (`https://api.ainote.dev/api/mcp`) — Claude Desktop · Claude Code · Cursor · Windsurf - **OpenAPI 3.1** (`https://api.ainote.dev/api/mcp/openapi.json`) — OpenAI Custom GPT Actions · LangChain remote tools · AutoGen · 기타 HTTP-first 에이전트 두 surface 가 **하나의 tool registry** 를 공유하므로 (`Api::McpController#apply_tool_annotations!`) 정의가 갈라질 일이 없다. 한 번 추가한 도구가 모든 에이전트 진영에서 즉시 보인다. ::: tip 2026-05-14 현재 상태 - ✅ 26 도구 production live + tool annotations 4-hint 부착 - ✅ npm `@ainote/mcp` v1.3.0 - ✅ OpenAPI 3.1 mirror at `api.ainote.dev/api/mcp/openapi.json` - 🔜 MCP resources / subscriptions (Phase 2), OAuth 2.1 DCR (Phase 3), A2A AgentCard (Phase 4) ::: --- ## Backend Completeness — 우리의 설계 철학 > "백엔드는 철저하게, 사용자는 간단하게." ainote 의 모든 결정은 이 한 줄을 따른다. | Backend (철저함) | Frontend / Agent (단순함) | |------------------|---------------------------| | 모든 상태 변화 추적 (created_at / reviewed_at / reviewed_by / auto_approved) | 사용자에게 필요한 것만 표시 | | 완전한 메타데이터 (rejection_reason / access_token / expires_at) | 빠른 의사결정 유도 (버튼 2-3개) | | 복잡한 비즈니스 로직 (자동수락 규칙 / 상태 머신 / 엣지 케이스) | 낮은 인지 부하 | | 모든 정보를 API 로 제공 (상태 / 상세 / 히스토리 / 감사로그) | DM 형태의 자연스러운 UX | | Audit · 분석 · 문제 추적 | 상태·상세 데이터·필터 노출 금지 | 이 분리가 어떻게 에이전트 친화성으로 이어지는가: - `handoff_list` 가 호출될 때 **백엔드는** 7일 초과 핸드오프를 알아서 purge 한다. **에이전트는** 단지 목록을 받을 뿐 cleanup 을 모른다 — 그러나 annotations 으로 `destructiveHint: true` 를 advertise 해서 자율 에이전트가 destructive call 임을 인식 가능 - `vault_sync(action: push)` 가 호출되면 **백엔드가** conflict 를 감지·해결한다. **에이전트는** `conflictResolution: merge|overwrite|abort` 한 번 결정만 - `login_and_get_key` 는 MCP key 가 없으면 **백엔드에서 자동 생성**한다. **에이전트는** 그냥 "key 받기" 만 요청 → [Backend Completeness Principle 전문 보기](/philosophy) --- ## 50+개 도구 매트릭스 | 카테고리 | 도구 | annotations | |---------|------|-------------| | **Tasks (5)** | `list_tasks` | 🔒 ♻️ | | | `create_task` | | | | `update_task` | ♻️ | | | `delete_task` | ⚠️ ♻️ | | | `list_categories` | 🔒 ♻️ | | **Dev Docs (7)** | `list_dev_docs` | 🔒 ♻️ | | | `get_dev_doc` | 🔒 ♻️ | | | `create_dev_doc` | | | | `update_dev_doc` | ♻️ | | | `pull_dev_docs` | 🔒 ♻️ | | | `delete_dev_doc` | ⚠️ ♻️ | | | `list_dev_categories` | 🔒 ♻️ | | **Onboarding (3)** | `signup_and_get_key` | 🌐 | | | `login_and_get_key` | 🌐 | | | `get_setup_guide` | 🔒 ♻️ | | **Vaults (5)** | `vault_list` | 🔒 ♻️ | | | `vault_create` | 🌐 | | | `vault_clone` | 🔒 ♻️ 🌐 | | | `vault_connect_status` | 🔒 ♻️ 🌐 | | | `vault_sync` | ⚠️ 🌐 | | **Sync (3)** | `sync_push` | ⚠️ ♻️ | | | `sync_pull` | 🔒 ♻️ | | | `sync_list` | 🔒 ♻️ | | **Handoffs (3)** | `handoff_save` | ♻️ | | | `handoff_list` | ⚠️ | | | `handoff_get` | ⚠️ | **범례**: 🔒 read-only · ⚠️ destructive · ♻️ idempotent · 🌐 open-world (외부 시스템 호출) `handoff_list` / `handoff_get` 가 destructive 인 이유: 호출 시 7-day stale purge 가 같이 돈다. 자율 에이전트가 cleanup 의도 없이 destructive 호출을 피하도록 마킹. → [전체 annotations 매핑 + 판단 기준](/reference/annotations) --- ## Surface 매트릭스 — 어디서든 같은 도구 | 진입점 | Protocol | URL / 명령 | 설정 가이드 | |--------|----------|-----------|-------------| | **Claude Code** | MCP HTTP | `https://api.ainote.dev/api/mcp` | [/agents/claude-code](/agents/claude-code) | | **Claude Desktop** | MCP stdio | `npx -y @ainote/mcp` | [/agents/claude-desktop](/agents/claude-desktop) | | **Cursor** | MCP stdio / HTTP | 동일 | [/agents/cursor](/agents/cursor) | | **Windsurf** | MCP stdio | 동일 | [/agents/windsurf](/agents/windsurf) | | **ChatGPT Custom GPT** | OpenAPI 3.1 | Actions → Import URL `https://api.ainote.dev/api/mcp/openapi.json` | [/agents/openai-custom-gpt](/agents/openai-custom-gpt) | | **LangChain / LangGraph** | OpenAPI | Python remote tool | [/agents/langchain](/agents/langchain) | | **AutoGen** | OpenAPI | 동일 | [/agents/langchain#autogen](/agents/langchain#autogen) | | **웹 앱** | HTML | `https://app.ainote.dev` | — | | **iOS / Android / macOS / Watch** | 네이티브 | App Store / Play Store | [/agents/mobile](/agents/mobile) | | **Chrome / Safari 확장** | 브라우저 | Chrome Web Store | [/agents/extensions](/agents/extensions) | | **Telegram** | Bot | Clawdbot | [/mcp/telegram](/mcp/telegram) | --- ## Dual-protocol 게이트웨이 — 어떻게 동작하나 **같은 코드 한 곳에서** 두 surface 가 갈라진다: ```ruby # app/controllers/api/mcp_controller.rb class Api::McpController < ApplicationController MCP_TOOL_ANNOTATIONS = { "list_tasks" => { readOnlyHint: true, idempotentHint: true, ... }, "delete_task" => { destructiveHint: true, idempotentHint: true, ... }, # ... 26개 전체 }.freeze def self.apply_tool_annotations!(tools) tools.each { |t| t[:annotations] = MCP_TOOL_ANNOTATIONS[t[:name]] } tools end end # app/controllers/api/mcp/openapi_controller.rb class Api::Mcp::OpenapiController < ApplicationController def mcp_tools Api::McpController.apply_tool_annotations!( Api::McpController.build_tools_array ) end end ``` → MCP `tools/list` 는 annotations 를 그대로 emit, OpenAPI mirror 는 동일 annotations 를 `x-mcp-annotations` extension 으로 노출. **두 surface 가 갈라질 수 없는 구조**. → [코드 상세](/reference/dual-protocol) --- ## Quickstart — 한 진영씩 ### Claude Code (HTTP, 권장) ```json { "mcpServers": { "ainote": { "type": "http", "url": "https://api.ainote.dev/api/mcp", "headers": { "Authorization": "McpKey " } } } } ``` → [전체 가이드](/agents/claude-code) · MCP key 발급은 Claude 에 "ainote 가입시켜줘" 한 줄. ### Claude Desktop (stdio, npm) ```json { "mcpServers": { "ainote": { "command": "npx", "args": ["-y", "@ainote/mcp"], "env": { "AINOTE_API_URL": "https://api.ainote.dev", "AINOTE_API_KEY": "" } } } } ``` → [전체 가이드](/agents/claude-desktop) ### OpenAI Custom GPT Actions 1. ChatGPT → Create a GPT → **Configure** → Actions → **Create new action** 2. Schema → **Import from URL**: `https://api.ainote.dev/api/mcp/openapi.json` 3. Authentication → API Key → Auth Type: **Custom**, Header: `Authorization`, Value: `McpKey ` → [전체 가이드](/agents/openai-custom-gpt) --- ## Multi-device 시나리오 ``` [MacBook · Claude Code] handoff_save({project:"logi", topic:"phase4", time:"1555", content:"..."}) → vault 에 handoffs/logi-phase4-1555-2026-05-14.txt 저장 (다른 디바이스 / 다른 세션) [Mac mini · Claude Code] handoff_get({project:"logi", topic:"phase4"}) → 동일 핸드오프 회수 → 작업 재개 (iPhone) ainote 앱 → 같은 vault → 같은 데이터 ``` 같은 패턴이 **태스크 / dev-doc / sync_push / vault_sync** 전부에 적용. 모든 device 가 같은 백엔드를 본다. --- ## Security & Open Source - **MIT License** — 자체 호스팅 가능 (Docker / Render / Fly.io) - **age E2E encryption** — `mcp` category (mcpServers + API keys) 는 클라이언트에서 age 로 암호화 후 업로드. 서버는 평문 모름 - **OS Keychain integration** — 디바이스별 age identity 가 macOS Keychain / libsecret / Credential Manager 에 저장 - **데이터 위치** — Render Singapore PostgreSQL + git vault (사용자 소유 GitHub repo) - **RFC 8628 device flow** — CLI 로그인 시 PKCE (S256) 강제 - **삭제 권리** — `delete_task` · `delete_dev_doc` 등 모든 destructive 도구 사용자 임의 호출 가능. 30일 trash window 후 영구 삭제 → [Privacy Policy](/legal/privacy) · [Terms of Service](/legal/terms) · [Security disclosure](/legal/security) --- ## 자주 묻는 질문 **Q. 계정 없이 바로 써볼 수 있나요?** — 네. MCP 등록 후 Claude 에 "ainote 가입 시켜줘" 한 줄이면 `signup_and_get_key` 가 호출돼서 키가 발급됩니다. **Q. Obsidian 사용자인데 마이그레이션 가능한가요?** — `vault_create` 로 vault 만든 뒤 git remote 로 기존 Obsidian vault 를 붙이면 됩니다. 마크다운이라 wikilinks `[[...]]` 그대로 호환. **Q. OpenAI Custom GPT Store 에 공개 가능한가요?** — Yes. `api.ainote.dev/api/mcp/openapi.json` 가 OpenAPI 3.1 spec, privacy policy URL (`ainote.dev/legal/privacy`) 의 root domain 이 일치 → verified domain DNS TXT 인증 완료 시 Public 배포 가능. **Q. 데이터 어디 저장되나요?** — 호스팅: Render(Singapore) PostgreSQL + 사용자 GitHub repo (vault). 셀프호스팅: 자체 인스턴스 가능. [데이터 내보내기](/guide/data-export) 언제든 가능. **Q. AI 가 모든 노트 다 보나요?** — MCP 도구 호출할 때만. 호출 단위는 너가 직접 승인 가능 (Claude Code 에서). 권한 모델: API key 단위로 read/write 분리 가능. **Q. Anthropic / OpenAI / Google A2A 다음 어디까지 지원?** — MCP (지금) → OpenAPI (지금) → A2A AgentCard (Phase 4 진행 중) → AGNTCY / Letta (모니터링). [전체 로드맵](/roadmap) **Q. Pricing?** — 자체 호스팅 무료 (MIT). 호스팅 plan TBD. [→ 전체 FAQ](/guide/faq) --- ## AI/LLM 에게 통째로 던지기 [llms.txt 표준](https://llmstxt.org/) 으로 전체 문서를 LLM 친화 형식으로 제공: - [📥 `/llms.txt`](/llms.txt) — 페이지 색인 + 1줄 요약 - [📥 `/llms-full.txt`](/llms-full.txt) — 모든 본문 한 파일로 ChatGPT/Claude 에 붙여넣고 "ainote 가 뭐고 어떻게 붙이는지 알려줘" 한마디면 끝. ---

Privacy · Terms · Security · Contact (Telegram · 카카오 오픈채팅)
MIT License · Built by Seunghan Kim · ainote.dev — 너의 노트는 너의 것.

--- # 계정 삭제 안내 **Last updated**: 2026-05-21 **Service**: ainote (AI Simplenote) **Operator**: 디코드(Dcode) · 대표 김다혜 — 사업자 정보는 [개인정보처리방침 §10](/legal/privacy#contact) 참조 ainote 계정은 **앱 내 설정**에서 직접 삭제하거나, 아래 이메일로 요청하여 삭제할 수 있다. --- ## 1. 앱 내에서 삭제하기 (가장 빠름) ### iOS / Android (AI Simplenote 앱) 1. 앱 실행 → 좌하단 **설정 (Settings)** 탭 2. **계정 (Account)** 섹션 진입 3. **계정 삭제 (Delete Account)** 버튼 탭 4. 확인 다이얼로그에서 **삭제** 선택 5. 즉시 로그아웃되며 삭제 절차 시작 ### 웹 (app.ainote.dev) 1. 우상단 프로필 → **설정** 2. **계정** 탭 → **계정 삭제** 3. 비밀번호 재확인 → **삭제 확인** --- ## 2. 이메일로 요청하기 앱에 접근할 수 없는 경우 아래 채널로 요청: - **이메일**: (비공개 문의 — 삭제 요청 권장 채널) - **GitHub Issues**: (공개 이슈 — 본인 확인 후 처리) - **Telegram / Kakao open chat**: (블로그 Contact 섹션) 요청 시 다음 정보 포함: - 가입 이메일 주소 - 계정 생성 시점 (대략) - 삭제 사유 (선택) 본인 확인 후 **영업일 기준 7일 이내** 처리된다. --- ## 3. 삭제되는 데이터 계정 삭제 요청 시 다음 데이터가 **영구 삭제**된다: | 분류 | 항목 | |---|---| | 계정 | 이메일, 비밀번호 해시, OAuth 연동(Google/Apple/1pass), 프로필 이미지 | | 사용자 콘텐츠 | 노트, 할 일, 카테고리, 캘린더 이벤트, 첨부 파일, 음성 메모 | | AI 사용 기록 | 채팅 히스토리, API 키(BYOK 저장 시), 사용량 카운터 | | 디바이스 | FCM/APNs 푸시 토큰, 세션 토큰, refresh token | | 통합 | Telegram 봇 연동, Chrome/Safari 확장 동기화 상태 | --- ## 4. 즉시 삭제 vs 보존 데이터 ### 즉시 삭제 (요청 즉시 또는 30일 이내) - 위 §3 의 **모든** 사용자 콘텐츠 및 식별 가능 정보 ### 법적 의무로 보존되는 데이터 - **결제 기록**: 전자상거래법에 따라 **5년** 보존 (구매 이력만, 카드 정보는 PG사 보관) - **로그인/접속 로그**: 통신비밀보호법에 따라 **3개월** 보존 (IP, 접속 시각) - **세금 신고 자료**: 부가가치세법에 따라 **5년** 보존 (해당 시) 이 데이터는 개인 식별이 불가능한 형태로 익명화되어 보존되며, 보존 기간 종료 후 자동 삭제된다. --- ## 5. 삭제 유예 기간 (Grace Period) 계정 삭제 후 **30일 동안** 계정이 비활성 상태로 보관되며, 같은 이메일로 재로그인하면 복구 가능하다. 30일 경과 시 모든 데이터가 영구 삭제되며, 이후 복구 불가하다. --- ## 6. 자주 묻는 질문 **Q. 일부 데이터만 삭제할 수 있나요?** A. 노트/할 일/카테고리는 각각 앱에서 개별 삭제 가능하다. 계정 자체를 유지하면서 모든 콘텐츠를 비우려면 [Settings → 데이터 → 모두 삭제] 를 사용한다. **Q. 다른 기기에서도 자동 삭제되나요?** A. 그렇다. 계정 삭제 시 동기화된 모든 기기 (iOS, Android, Web, 확장 프로그램, Apple Watch) 에서 데이터가 사라진다. **Q. AI 채팅 기록은 외부 LLM 제공자에게도 전달되나요?** A. BYOK (Bring Your Own Key) 모드는 사용자가 직접 등록한 키로 호출되므로 ainote 서버에 저장되지 않는다. 서버 폴백 (월 3회 무료) 사용 시 ainote 서버 로그에 남으며, 계정 삭제 시 함께 제거된다. 외부 LLM 제공자 (Anthropic, OpenAI, Google) 의 보존 정책은 각 제공자의 정책을 따른다. **Q. 푸시 알림 토큰만 해제하고 싶어요.** A. [Settings → 알림 → 알림 끄기] 를 사용한다. 계정 삭제까지 불필요하다. --- ## 7. 문의 - 삭제 요청 또는 처리 지연 문의: 위 §2 의 채널 - 개인정보 처리방침 전반: [/legal/privacy](/legal/privacy) - 보안 취약점 신고: [/legal/security](/legal/security) --- > 본 안내는 한국어 원본을 기준으로 한다. 영문 번역과 충돌 시 한국어 본문 우선. --- # OpenAPI & Domain Verification Log ainote 의 OpenAPI 3.1 mirror 와 도메인 인증 상태 기록. OpenAI Custom GPT Store, marketplace 등록 심사 시 참조용. --- ## OpenAPI 3.1 mirror - **Spec URL**: - **Generator**: `Api::Mcp::OpenapiController` (Rails 8) - **Source**: same tool registry as `Api::McpController#tools/list` - **Tool annotations**: `x-mcp-annotations` extension (`readOnlyHint` / `destructiveHint` / `idempotentHint` / `openWorldHint`) - **Auth scheme**: `McpKeyAuth` (header `Authorization: McpKey `) - **Tools count**: 26 (2026-05-14 기준) - **First publish**: 2026-05-14 commit `4854550fa` ## Domain Mapping | Domain | Service | First mapped | Status | |--------|---------|--------------|--------| | `app.ainote.dev` | ainote web (`srv-d1h0cpripnbc73b8j6p0`) | 2026-02-22 | ✅ verified | | `api.ainote.dev` | ainote web (`srv-d1h0cpripnbc73b8j6p0`) | 2025-08-01 | ✅ verified | | `docs.ainote.dev` | ainote-docs (`srv-d7ttcagsfn5c73cfdi30`) | 2026-05-07 | ✅ verified | | `ainote.dev` (apex) | (Netlify 블로그, 별개 서비스) | — | 본 docs 와 무관 | | `www.ainote.dev` | (Netlify 블로그, 별개 서비스) | — | 본 docs 와 무관 | 마케팅 + 문서 surface 는 `docs.ainote.dev` 가 단일 진입점. apex (`ainote.dev`) 와 `www.ainote.dev` 는 기존 Netlify 블로그가 사용 중이라 본 docs 와 분리. ## OpenAI Custom GPT — Verified Domain OpenAI 는 Custom GPT 의 privacy policy URL hostname 의 root domain (`ainote.dev`) DNS TXT 인증을 요구한다. ### 인증 절차 1. **ChatGPT 에서 TXT 값 발급** — → 좌하단 프로필 → Settings → **Builder profile** → **Verified domain** → Add → 도메인 `ainote.dev` 입력 → OpenAI 가 `openai-domain-verification=dv-<랜덤>` TXT record 제공 2. **GoDaddy 에 TXT 추가** — DNS 패널 → 신규 레코드 추가 - 유형: `TXT`, 이름: `@`, 데이터: `openai-domain-verification=dv-...` (Step 1 값 그대로), TTL: 1시간 3. **검증 클릭** — ChatGPT 로 돌아가 Verify → DNS 전파 (5분~24시간) → ✓ verified 4. 그 후 모든 Custom GPT 의 Privacy policy URL = `https://docs.ainote.dev/legal/privacy` 가능, Action URL = `https://api.ainote.dev/...` 가능 ### Custom GPT Privacy URL 정책 일치 OpenAI 는 다음 두 hostname 의 root domain 이 같아야 verified 도메인 사용 가능: - Privacy policy URL host: `docs.ainote.dev` → root `ainote.dev` ✅ - OpenAPI server URL host: `api.ainote.dev` → root `ainote.dev` ✅ - Verified domain (ChatGPT 인증): `ainote.dev` ✅ (모두 일치) ### 인증 진행 상태 - Privacy Policy URL: `https://docs.ainote.dev/legal/privacy` (page live since 2026-05-14, commit `b7ba25ffa`) - Verification request 일자: 2026-05-14 - TXT value: `openai-domain-verification=dv-ltBoPxu2ndZN8icGMhYpr83u` - DNS TXT added at GoDaddy: 2026-05-14 — propagation confirmed on 8.8.8.8 / 1.1.1.1 / system resolver - **Verified at ChatGPT: 2026-05-14 ✅** - 첫 Custom GPT 공개: TBD --- ## Tool surface 변경 이력 각 minor version 의 tool 변경 (추가 / 제거 / annotation 변경) 기록. | Date | Change | Commit | |------|--------|--------| | 2026-05-14 | OpenAPI 3.1 mirror published; 26 tools | `4854550fa` | | 2026-05-14 | Annotations 4-hint 부착 on all 26 tools | `d588fe796` | | 2026-05-14 | `handoff_save` / `handoff_get` add `time` HHMM param | `b3fd9f04c` | `tools_count` 변경 시 본 표에 추가. --- ## Marketplace 등록 진행 상황 [`docs/todo/MARKETPLACE_REGISTRATION_KIT.md`](https://github.com/seunghan91/ainote/blob/main/docs/todo/MARKETPLACE_REGISTRATION_KIT.md) 참조 — 9개 디렉토리 등록 자료 + 등록 로그. --- # Privacy Policy **Last updated**: 2026-08-08 **Service**: ainote — iOS · Android 앱, 웹 서비스(`app.ainote.dev`), API(`api.ainote.dev`), Chrome / Safari 확장, MCP 도구 **Operator**: 디코드(Dcode) · 대표 김다혜 — 사업자 정보는 [§10](#contact) 참조 > 이 문서가 ainote 개인정보처리방침의 **정본**이다. > 다른 경로에 게시된 사본과 내용이 다르면 이 문서가 우선한다. --- ## 1. 어떤 데이터를 수집하는가 ### 사용자 계정 - 이메일 주소 (가입 시) - 비밀번호 hash (bcrypt — 평문 비밀번호 저장 안 함) - (선택) 표시 이름 ### 사용자 컨텐츠 - 태스크 (`tasks`), dev-docs, 핸드오프, vault 파일 — **사용자가 직접 작성한 모든 내용** - 위 항목의 메타데이터 (created_at, updated_at, category, position 등) - **음성 파일** — 사용자가 회의록 기능에서 직접 고른 오디오 파일(m4a·mp3·wav·webm). 전사·요약을 위해 서버로 올라가고 §4-2 의 AI 사업자로 전달된다. 앱이 직접 녹음하지는 않는다 — 기기에 이미 있는 파일을 불러온다. ### 인증·세션 - MCP API keys (해시 저장 — 평문은 발급 직후 한 번만 노출) - JWT 토큰 (RFC 8628 device flow 시), 발급 일시 + 만료 일시 - OAuth Apple / Google ID 토큰 (SSO 사용 시) - 세션 IP / User-Agent (보안 감사 목적, 30일 후 자동 삭제) ### 외부 통합 데이터 - GitHub installation ID (vault 기능 사용 시) - Apple / Google federated identity (SSO 사용 시) - Telegram chat ID (Telegram bot 연동 시) - **기기 캘린더** (iOS·Android 앱에서 사용자가 캘린더 권한을 허용한 경우에만): - 앱이 기기에 이미 있는 일정을 **읽고**, 사용자가 만든 일정을 기기 캘린더에 **쓴다**. - 🔴 **이 데이터는 기기 밖으로 나가지 않는다.** ainote 서버로도, 제3자로도 보내지 않는다. 아래 §4 의 Google Calendar 행과는 별개 경로다 — 그쪽은 웹에서 계정을 연결한 경우에만 동작한다. - 권한을 허용하지 않아도 나머지 기능은 그대로 쓸 수 있다. - **Google Calendar 연동 데이터** (웹에서 사용자가 설정에서 직접 활성화한 경우에만): - 캘린더 목록 (이름, 시간대, 색상) — 동기화 대상 캘린더 선택 UI 표시 목적 - 일정 정보 (제목, 설명, 시간, 장소, 참석자) — 앱 내 일정 표시 + 할 일 ↔ 일정 동기화 목적 - Google OAuth access / refresh token (서버 저장, 연동 해제 시 즉시 삭제) - 요청 스코프: `calendar.calendarlist.readonly` (캘린더 목록 읽기 전용) + `calendar.events` (일정 조회·생성·수정·삭제) - **Google Drive 백업** (Android 앱에서 사용자가 직접 활성화한 경우에만): - 요청 스코프: `drive.file` — ainote 가 직접 생성한 백업 파일에만 접근, 다른 Drive 파일은 읽기 불가 ### 클립보드 (텍스트·이미지) ainote 는 **"클립보드로 할 일 만들기"** 기능을 위해 기기의 클립보드를 읽는다. 읽는 시점과 범위는 아래와 같고, 이 기능을 쓰지 않으면 내용을 읽지 않는다. | 시점 | 무엇을 | 어디까지 나가는가 | |---|---|---| | 앱 화면에 머무는 동안 | 클립보드에 **텍스트·이미지가 있는지 여부만** 확인 (버튼을 보여줄지 판단) | 기기 밖으로 나가지 않음 | | 사용자가 버튼을 누른 순간 | 클립보드의 **텍스트 본문**, Android·웹은 **이미지**도 함께 | ainote 서버 → AI 사업자 (아래 §4) | - **iOS**: 버튼 표시 판단에는 "글자가 있는가 / 이미지가 있는가"만 확인한다. 이 확인은 붙여넣기 확인창을 띄우지 않는다. 실제 내용은 버튼을 누른 뒤 한 번만 읽는다. iOS 는 텍스트만 전송하고 이미지는 전송하지 않는다. - **Android**: 화면이 떠 있는 동안 클립보드 변경을 감지해 텍스트·이미지 유무를 앱 안에서만 갱신한다. 버튼을 누르면 텍스트와 이미지를 서버로 보낸다. - **웹(app.ainote.dev)**: 브라우저의 클립보드 읽기 권한을 사용해, 사용자가 기능을 실행한 시점에만 텍스트·이미지를 읽는다. 전송된 클립보드 내용은 할 일을 뽑아내는 데만 쓰이고, 결과로 만들어진 할 일이 계정에 저장된다. 클립보드 원본 자체를 별도 항목으로 보관하지 않는다. 클립보드를 상시 감시하거나, 다른 앱의 복사 이력을 수집하지 않는다. ### 자동 수집 (서버 로그) - API 호출 메서드 / 응답 코드 / 응답 시간 - ⚠️ AI 기능(요약·할 일 추출)을 호출하면 **입력 텍스트의 앞부분이 서버 로그에 남는다.** 로그는 운영 진단 목적으로만 보관한다. ### 이용 분석 (선택 동의) - **모바일 앱**: 기본값 **꺼짐**. 설정에서 직접 켠 경우에만 화면 조회·기능 사용 이벤트를 PostHog 로 보낸다. 익명 식별자를 쓰고, 노트·할 일 본문은 보내지 않는다. - **서버**: 로그인·할 일 생성/수정/삭제, AI 요청 같은 **행동 이벤트**를 PostHog 로 보낸다. 계정 식별자와 이벤트 속성(카테고리 id, 변경된 필드 이름, 글자 수, 처리 시간)이 포함되고, 노트·할 일 본문은 포함하지 않는다. - **웹(app.ainote.dev)**: PostHog 웹 SDK 로 페이지 조회를 수집한다. - **Android 앱**: Firebase Analytics·Crashlytics 가 앱 실행·화면 조회·크래시 정보를 자동 수집한다. --- ## 2. 데이터 위치 - **Primary database**: PostgreSQL (Render, Singapore region) - **Encryption at rest / in transit**: Render Postgres 는 **AES-256** 으로 저장 시 암호화되며 복제본과 백업에도 같이 적용된다. 외부 연결은 Render 관리 TLS 인증서로 전송 구간이 암호화된다 (출처: [Render 문서](https://render.com/docs/postgresql-creating-connecting)). 이는 인프라 수준 암호화이고, **애플리케이션 수준의 추가 암호화는 아래 `mcp` category 에 한정**된다 — 종단간 암호화(E2EE)가 전체 데이터에 적용된다는 뜻이 아니다 - **Vault git repositories**: 사용자 본인 소유 GitHub 계정 (private repo). ainote 는 indexing 만, git traffic 은 proxy 안 함 - **Encrypted client-side data**: `mcp` category (mcpServers + API keys) 는 클라이언트 디바이스에서 **age** 로 암호화 후 업로드. 서버는 평문 모름 - **OS Keychain**: 디바이스별 age identity / MCP key 는 사용자 본인 디바이스의 macOS Keychain / libsecret / Credential Manager --- ## 3. 데이터 사용 ainote 는 사용자 데이터를: ✅ **사용**: - 사용자가 명시적으로 호출한 도구의 결과 반환 - 멀티-디바이스 동기화, 사용자 본인 식별, 보안 감사 - 사용자가 실행한 AI 기능(요약·전사·할 일 추출·이미지 분석)의 처리 - 알림 발송 (리마인더·브리핑) - 기능 개선을 위한 이용 통계 — 위 §1 "이용 분석" 범위 안에서만 ❌ **사용 안 함**: - 광고 목적 이용 — 0. 광고 네트워크·데이터 브로커에 제공 — 0 - 제3자 판매 — 0 - **회사가 직접 수행하는** AI/ML 모델 학습 — 0 - 마케팅 이메일 — 0 (서비스 필수 알림만) AI 기능을 실행하면 그 대상 콘텐츠가 §4 의 AI 사업자에게 전송된다. 전송받은 데이터를 각 사업자가 자사 약관에 따라 어떻게 처리하는지는 해당 사업자의 정책을 따르며, ainote 의 통제 범위 밖이다. ### Google 사용자 데이터 — Limited Use 고지 ainote's use and transfer to any other app of information received from Google APIs will adhere to the [Google API Services User Data Policy](https://developers.google.com/terms/api-services-user-data-policy), including the **Limited Use** requirements. - Google Calendar / Drive 데이터는 위에 명시된 사용자-노출 기능(일정 표시, 할 일 동기화, 백업) 제공에만 사용 - 광고 목적 사용 금지, 제3자 판매·이전 금지 - 사람의 열람 금지 (사용자의 명시적 동의, 보안 점검, 법적 의무 등 정책상 허용된 예외 제외) - AI/ML 모델 학습에 사용하지 않음 --- ## 4. 제3자 공유 개인정보를 판매하거나 임대하지 않는다. ### 4-1. 서비스 운영 기반 | 제3자 | 무엇을 | 왜 | |------|------|----| | Render (호스팅) | 모든 데이터 (저장 위치) | 인프라 제공자, Singapore 데이터센터 | | 1pass (api.1pass.dev) | 인증에 필요한 계정 식별 정보 | 통합 로그인(SSO). 같은 운영주체의 처리위탁 | | Apple / Google (SSO) | 인증 토큰만 | SSO 로그인 사용자 한정 | | Sentry | 예외 메시지·스택트레이스, 느린 SQL 질의, 요청 경로, 성능 프로파일 | 오류 진단. 요청 본문·쿠키·IP 는 보내지 않도록 설정 | | PostHog | 계정 식별자, 행동 이벤트 이름과 속성 | 이용 통계 (§1 "이용 분석") | | Firebase (Analytics · Crashlytics) | 앱 실행·화면 조회 이벤트, 크래시 로그, 이용자 식별자 | Android 앱 오류 수집·이용 통계 | ### 4-2. AI 기능을 실행할 때 요약·전사·분석·**클립보드 할 일 추출**을 실행하면, 그 대상이 되는 콘텐츠가 해당 기능을 제공하는 AI 사업자로 전송된다. 기능을 쓰지 않으면 전송되지 않는다. | 제3자 | 무엇을 | 왜 | |------|------|----| | BizRouter (bizrouter.ai) | **클립보드 텍스트·이미지**, AI 대화 전문과 이력, 관련 할 일 맥락, YouTube 요약 | AI 게이트웨이. 최종 처리는 Google·OpenAI·Anthropic·Perplexity 로 재위탁 | | Google (Gemini API) | 음성 녹음 원본, 웹페이지 본문, YouTube 자막, 노트·할 일 텍스트, 이미지 | 요약·분석·음성 전사·임베딩 | | Groq | 오디오 파일 원본 | 음성 전사 (1순위 경로) | | OpenAI | 이미지, 오디오 파일, 대화 내용 | 이미지 분석, 음성 전사, 텔레그램 대화 | | Anthropic | 이미지, 대화 내용 | 이미지 분석, 텔레그램 대화 | | Voyage AI · OpenRouter · Ollama | 노트·위키 본문 | 사용자가 본인 API 키를 등록한 경우에만 | | Anthropic · OpenAI (**BYOK 직결**) | AI 에이전트에 보낸 질문과 대화 이력 전문 | 🔴 사용자가 본인 API 키를 등록한 경우에만. **앱이 해당 서비스에 직접 연결하며 ainote 서버를 거치지 않는다** — 위 §4-2 의 서버 경유 행과는 다른 경로다. 키는 기기 보안 저장소에만 있고 서버로 보내지 않는다 | ### 4-3. 알림·연동 기능 | 제3자 | 무엇을 | 왜 | |------|------|----| | Google (FCM) | 알림 제목과 본문 — **할 일 제목과 메모가 그대로 담긴다** | Android·웹 푸시 발송 | | Apple (APNs · Sign in with Apple) | 알림 내용, 인증 토큰 | iOS 푸시 발송, Apple 로그인 검증 | | Expo · 브라우저 푸시 서비스 | 알림 내용 | 푸시 발송 (대체 경로) | | Telegram | 연동된 대화의 메시지 내용 | 텔레그램 연동을 켠 경우에만 | | Google Calendar API | 일정 데이터 (양방향 동기화) | **웹에서 캘린더 연동을 직접 활성화한 사용자 한정.** 앱의 기기 캘린더(§1)는 이 경로를 타지 않는다 | | GitHub (vault) | 연동 대상 노트 파일의 내용. 사용자 본인 소유 repo 에만 | vault 기능을 쓰는 경우에만 | | Naver Cloud | 사용자가 입력한 장소 문자열 | 할 일 장소의 좌표 변환 | ### 4-4. 국외 이전 위 수신자는 대부분 국외에 서버를 두고 있어, 해당 기능을 쓰면 개인정보가 국외로 이전된다. 이전 항목과 목적은 각 표에 적힌 것과 같고, 이전 국가는 수신자의 서비스 지역을 따른다. 이전은 서비스 제공에 필요한 범위에서만 이루어지며, 해당 기능을 쓰지 않는 방법으로 이전을 피할 수 있다. --- ## 5. 사용자 권리 ### 데이터 접근 / 이동 - `GET /api/users/me/export` (계획 — Phase 3) — 모든 사용자 데이터 JSON dump - `pull_dev_docs` 도구로 dev-doc 즉시 로컬 복원 - `vault_clone` 으로 vault git repo URL 발급, 본인 GitHub 에서 직접 clone ### 데이터 삭제 - 모든 destructive 도구 사용자 임의 호출 가능 (`delete_task`, `delete_dev_doc` 등) - 30일 trash window 후 영구 삭제 (`TaskCleanupJob` 매일 2am KST) - 계정 전체 삭제: 앱 또는 [app.ainote.dev](https://app.ainote.dev) → 설정 → 계정 삭제. 요청 후 **30일** 동안 비활성 상태로 보관되고, 그 기간에 재로그인하면 복구된다. 30일이 지나면 영구 삭제되며 복구할 수 없다. 상세: [/legal/account-deletion](/legal/account-deletion) ### 데이터 수정 - 모든 update 도구 (`update_task`, `update_dev_doc` 등) - 잘못된 핸드오프: 7일 자동 purge 또는 같은 (project, topic, date, time) 으로 overwrite ### GDPR (EU 사용자) - Right to access — 위 export - Right to rectification — 위 수정 - Right to erasure ("right to be forgotten") — 위 삭제 - Right to data portability — JSON export + vault git clone - Right to object — 마케팅·프로필링 없음 (애초에 사용 안 함) - 처리 근거: Article 6(1)(b) "necessary for the performance of a contract" - Data Protection Officer (DPO): 직접 운영 — [Contact](#contact) --- ## 6. 쿠키 / 추적 - **app.ainote.dev (웹 앱)**: session cookie(필수) + PostHog 웹 SDK 가 두는 분석용 저장값. 광고 쿠키 0 - **ainote.dev · docs.ainote.dev (랜딩 + 문서)**: 쿠키 0 - **api.ainote.dev (MCP/OpenAPI API)**: 쿠키 0, header-based auth 만 - **모바일 앱**: 광고 식별자(IDFA·GAID) 를 읽지 않는다. 이용 분석은 §1 의 선택 동의 범위 --- ## 7. 보안 - HTTPS 강제 (Render 가 발급한 Let's Encrypt 인증서) - 비밀번호 bcrypt (cost factor 12+) - MCP key 발급 시 평문은 단 한 번만 노출, 이후 hash 저장 - E2E age 암호화 (`mcp` category) - 보안 취약점 보고: [/legal/security](/legal/security) --- ## 8. 미성년자 ainote 는 만 14세 미만 사용자를 대상으로 하지 않는다. 만 14세 미만임이 확인되면 즉시 계정 삭제 + 데이터 삭제. --- ## 9. 정책 변경 본 정책 변경 시 `Last updated` 일자가 갱신된다. 중대한 변경 (데이터 사용 범위 확대 등) 시 활성 사용자에게 in-app 알림. 30일 grace period. --- ## 10. Contact { #contact } ### 운영주체 (개인정보처리자) | 항목 | 내용 | |---|---| | 상호 | 디코드(Dcode) | | 대표자 | 김다혜 | | 사업자등록번호 | 280-18-02841 | | 통신판매업 신고번호 | 2026-강원원주-00196 | | 사업장 소재지 | 강원특별자치도 원주시 동부순환로 261, 4층 4호 A54 (행구동) | | 대표 전화 | 010-4275-7750 | ### 문의 채널 - **이메일**: - **GitHub Issues**: - **Telegram / Kakao open chat**: (블로그의 Contact 섹션에서 채널 확인) - **Security disclosure**: [/legal/security](/legal/security) --- > 본 정책은 한국어 원본을 기준으로 한다. 영문 번역과 충돌 시 한국어 본문 우선. --- # Security Disclosure **Last updated**: 2026-05-14 --- ## 1. 보고 채널 보안 취약점을 발견했을 때: ### 비공개 (권장) - **Telegram / Kakao 1:1**: 의 Contact 섹션 채널로 DM - 메시지 제목 `[ainote security]` 로 시작 - 가능하면 PoC + 영향 범위 + 재현 단계 포함 ### GitHub Security Advisory (오픈소스 코드 취약점) - - 비공개 draft 로 보고 → 협의 후 공개 ### 절대 하지 말 것 - 공개 GitHub Issue 에 취약점 상세 공개 (조정 전) - 제3자에게 취약점 정보 판매 / 공유 - 실 사용자 데이터에 대한 unauthorized access 시도 --- ## 2. 응답 SLA (best-effort) | 심각도 | 1차 응답 | 패치 목표 | |--------|---------|----------| | Critical (실 사용자 데이터 유출 가능) | 24시간 | 7일 | | High (인증 우회, 권한 상승) | 48시간 | 14일 | | Medium (특정 조건 하 데이터 누설) | 7일 | 30일 | | Low (정보 누설 없는 결함) | 14일 | 90일 | 운영 인력이 1인이므로 SLA 는 best-effort. 일정 지연 시 보고자에게 일정 공유. --- ## 3. 책임 있는 공개 (responsible disclosure) - 보고자가 90일 이상 패치를 기다린 경우 공개 가능 — Google Project Zero 표준 따름 - 패치 배포 후 보고자 동의 하에 (또는 anonymously) credit - 명예의 전당: [GitHub README](https://github.com/seunghan91/ainote#security-credits) (계획) --- ## 4. Bug bounty 현재 공식 bug bounty 프로그램 없음. 상황에 따라 운영자가 임의 사례 가능. --- ## 5. 암호화 모델 ### Transit - 모든 트래픽 HTTPS (Render Let's Encrypt 자동 발급) - HSTS 헤더 (계획 — Phase 3) - TLS 1.2+ 강제 ### At-rest - PostgreSQL 디스크 암호화 (Render 인프라 레벨) - 비밀번호 bcrypt (cost 12+) - MCP key 발급 시 평문은 한 번만 노출 후 hash 저장 ### Client-side E2E (mcp category) 사용자의 `~/.claude.json` 의 `mcpServers` 항목 + API key 들은 ainote 클라우드 동기화 시 **age** 로 클라이언트에서 암호화 후 업로드. 서버는 평문 모름. - Age identity: 디바이스별 keypair, OS Keychain 저장 - Recipients: 사용자의 다른 디바이스 public key 들 - 디바이스 추가 시 `sync add_recipient ` 으로 갱신 → [age encryption 상세](/security/age) (계획) --- ## 6. 의존성 보안 - npm 패키지 `@ainote/mcp` 는 `npm audit` 자동 검사 - Rails gem 은 `bundle audit` 정기 실행 - 알려진 CVE 영향 시 critical/high 는 7일 내 패치 - 의존성 lock files (package-lock.json, Gemfile.lock) commit --- ## 7. 인프라 - Render Singapore region (PostgreSQL + web service) - Render 인프라 보안 정책: - 서비스 SSH 접근: 운영자 1명, hardware-backed key - DB credentials: Render secret store, repo 에 commit 안 함 --- ## 8. 취약점 카테고리별 안내 ### XSS / CSRF / SQLi - Rails 8 의 built-in 방어 사용 - ContentSecurityPolicy (CSP) 헤더 (계획 — Phase 3) ### Auth / Session - Devise (web) + JWT (mobile / MCP) - MCP key 는 단순 secret string, JWT 가 아님 — revocation 가능 - OAuth 2.1 + DCR (Phase 3 도입 예정) — 동적 client registration ### Multi-tenancy - 사용자별 격리: PostgreSQL row-level (`user_id` foreign key) + 모든 query 에 scope - vault 격리: 사용자 본인 GitHub repo + ainote installation 권한만 ### Rate limit / DoS - Phase 3 후 정식 도입 (`X-RateLimit-*` headers, `Retry-After`) - 현재는 Render 인프라 레벨 보호 --- ## 9. 이전 보안 이슈 (공개 가능 항목) (현재 공개된 항목 없음) --- ## 10. Contact - **비공개 보고 권장**: 의 Telegram/Kakao 채널 - **GitHub Security**: --- # Terms of Service **Last updated**: 2026-05-14 **Service**: ainote (`ainote.dev`, `app.ainote.dev`, `api.ainote.dev`) **Operator**: 디코드(Dcode) · 대표 김다혜 — 사업자 정보는 [§11](#_11-contact) 참조 --- ## 1. 두 가지 분리된 것 ### Code (오픈소스) ainote 의 소스 코드는 [MIT License](https://github.com/seunghan91/ainote/blob/main/LICENSE) 하에 공개된다. 누구나 fork / modify / self-host 가능. ainote 운영자는 코드 사용에 대한 보증을 제공하지 않는다 — MIT 라이선스 본문 따름. ### Hosted Service (이 약관 대상) `ainote.dev` / `app.ainote.dev` / `api.ainote.dev` 에서 디코드(Dcode) 가 운영하는 호스팅 서비스. 본 약관은 호스팅 서비스 사용에 적용된다. 자체 호스팅 인스턴스는 본 약관 대상이 아니다 — MIT 코드로 본인 서버 운영 시 본인 책임. --- ## 2. 계정 - 만 14세 이상만 사용 가능 - 한 사람당 다중 계정 허용 (예: 개인용 / 업무용 분리) - 가입 시 제공한 이메일 주소가 사용자 본인 소유여야 함 - 비밀번호 / MCP key 보안 책임은 사용자 본인 — 분실 / 유출 시 즉시 회전 --- ## 3. 허용·금지 행위 ### 허용 - 사용자 본인 컨텐츠 생성 / 수정 / 삭제 / 공유 - API / MCP / OpenAPI 를 통한 자동화 호출 (rate limit 내) - 본인 vault 의 git repo 외부 사용 (clone / push 자유) - 데이터 export 후 다른 서비스로 마이그레이션 ### 금지 - 타인의 데이터에 무단 접근 시도 (사용자 격리 우회) - 인프라 abuse (DoS, 스팸, 무한 루프) - 불법 / 유해 컨텐츠 저장 (CSAM, 폭력 선동, 사기 등) - 본 약관 / 적용 법률 위반 행위 - 타인 계정 도용 / MCP key 무단 사용 위반 시 사전 통보 없이 계정 정지 / 데이터 삭제 가능. --- ## 4. Rate limit / Fair use - 호스팅 서비스는 fair use 기준으로 무료 제공 - 단일 사용자가 인프라에 부담을 주는 패턴 (분당 수천 호출 등) 감지 시 일시적 차단 가능 - 정식 rate-limit 헤더 (`X-RateLimit-*`) 는 Phase 3 후 도입 예정 - 대량 사용 필요 시 [Contact](/legal/privacy#contact) 로 협의 --- ## 5. 사용자 데이터 소유권 - **모든 사용자 컨텐츠의 소유권은 사용자 본인** — ainote 운영자는 단지 인프라 제공자 - ainote 운영자는 사용자 컨텐츠를 다음 외 목적으로 사용하지 않는다: - 사용자 본인의 도구 호출 결과 반환 - 멀티-디바이스 동기화 - 보안 감사 (PII 없는 메타데이터만) - 자세한 사용 / 비-사용 정책은 [Privacy Policy](/legal/privacy) --- ## 6. 서비스 가용성 - ainote 는 best-effort 가용성을 제공한다 — SLA 보장 없음 - 계획된 점검 / 장애 / 인프라 마이그레이션 시 일시 중단 가능 - 호스팅 인프라 (Render, GitHub, OS keychain 등) 의 장애는 ainote 운영자의 통제 밖 - 사용자 데이터 손실 위험 완화: vault git repo 는 사용자 본인 GitHub 계정에 존재 → ainote 서버 손실 시에도 vault 는 유지됨 --- ## 7. 면책 / 책임 한계 - ainote 호스팅 서비스는 "as-is" 제공 - 데이터 손실 / 비즈니스 손실 / 결과적 손해 등에 대한 책임은 사용자가 지불한 금액 (무료 plan = $0) 한도 내 - 사용자가 본인 vault 의 GitHub repo 를 임의 삭제하거나, 자체 OS keychain 의 age identity 를 분실한 경우 ainote 운영자는 복구 불가 --- ## 8. 종료 ### 사용자가 종료 - [app.ainote.dev](https://app.ainote.dev) → Settings → Delete account - 7일 grace period 후 영구 삭제 (그 사이 같은 이메일로 재가입 시 데이터 복구 옵션 제공) - vault git repo 는 사용자 본인 GitHub 계정에 그대로 남음 ### 운영자가 종료 - 약관 위반 시 즉시 정지 가능 - 서비스 종료 결정 시 최소 30일 전 활성 사용자에게 in-app 알림 + 데이터 export 안내 --- ## 9. 분쟁 해결 - 적용 법률: 대한민국 - 분쟁 시 우선 협의 — [Contact](/legal/privacy#contact) - 협의 불성립 시 서울중앙지방법원 1심 전속관할 --- ## 10. 약관 변경 본 약관 변경 시 `Last updated` 일자가 갱신된다. 중대한 변경 시 활성 사용자에게 in-app 알림 + 30일 grace period. 지속 사용 = 변경된 약관에 동의. --- ## 11. Contact ### 운영주체 | 항목 | 내용 | |---|---| | 상호 | 디코드(Dcode) | | 대표자 | 김다혜 | | 사업자등록번호 | 280-18-02841 | | 통신판매업 신고번호 | 2026-강원원주-00196 | | 사업장 소재지 | 강원특별자치도 원주시 동부순환로 261, 4층 4호 A54 (행구동) | | 대표 전화 | 010-4275-7750 | ### 문의 채널 - **GitHub Issues**: - **Telegram / Kakao open chat**: --- > 본 약관은 한국어 원본을 기준으로 한다. 영문 번역과 충돌 시 한국어 본문 우선. --- # ChatGPT 연결 (SSE) ChatGPT (Plus/Pro/Team) 의 MCP connector 로 ainote 사용. ::: warning ChatGPT MCP 는 SSE 전용 ChatGPT 의 connector 는 Server-Sent Events 만 지원. ainote hosted HTTP 직접 연결 불가 → 로컬 SSE 브리지 사용. ::: ## 1. 브리지 설치 ```bash npm install -g @ainote/mcp ``` 설치하면 두 명령이 생김: - `ainote-mcp` — stdio (Claude Desktop 용) - `ainote-mcp-http` — SSE 브리지 (ChatGPT 용) ## 2. 브리지 실행 ```bash export AINOTE_API_URL="https://api.ainote.dev" # 빌트인 default 는 구 onrender 호스트 — 꼭 지정 export AINOTE_API_KEY="h7Axq9XPsDTD2qr5yqtcCSaQ..." export AINOTE_MCP_HTTP_PORT=8765 # 기본 3030 — 원하는 포트로 override ainote-mcp-http ``` 출력: ``` ainote MCP SSE bridge listening on http://localhost:8765 SSE endpoint: http://localhost:8765/sse ``` ::: tip 포트 설정 `ainote-mcp-http` 는 `--port` 같은 CLI flag 를 받지 않습니다. **`AINOTE_MCP_HTTP_PORT` 환경변수**로 설정. 미설정 시 기본 **3030** 포트. ::: ::: tip 백그라운드로 항상 켜두기 brew services 또는 launchd plist 로 부팅 시 자동 실행 가능. [셀프호스팅 가이드](/guide/troubleshooting) 참고. ::: ## 3. ChatGPT 에 등록 ChatGPT 설정: 1. → "Connectors" 2. "Add connector" → "MCP server" 3. 입력: - **Name**: ainote - **URL**: `http://localhost:8765/sse` - **Auth**: (브리지가 env 로 처리, 비워둬도 됨) 4. "Connect" ::: warning ChatGPT 는 localhost 접근 가능? 브라우저에서 직접 접근 OK. 회사 방화벽 등이 막으면: - `localhost` 대신 `127.0.0.1` 사용 - HTTPS 가 필요하면 caddy / nginx 등 별도 리버스 프록시로 감싸세요. `ainote-mcp-http` 자체는 TLS 옵션이 없습니다. ::: ## 4. 사용 ChatGPT 에서 도구 사용 가능 표시 → 자연어로 호출: ``` ainote 에 "ChatGPT 테스트" 태스크 추가해줘 ``` ChatGPT 가 호출 전 승인 화면 띄움 → "Allow" 클릭. ## OAuth Bearer 인증 (선택) 기본은 env var (`AINOTE_API_KEY`) 입니다. ChatGPT 가 자체 `Authorization: Bearer …` 헤더로 호출하길 원하면 브리지를 OAuth bearer 검증 모드로 띄울 수 있습니다: ```bash export AINOTE_API_URL="https://api.ainote.dev" export AINOTE_API_KEY="..." export AINOTE_MCP_HTTP_PORT=8765 export AINOTE_ENABLE_OAUTH_AUTH=true ainote-mcp-http ``` 이러면 브리지가 들어오는 Bearer 토큰을 검증합니다. **브리지는 OAuth 인가 서버(authorize / token endpoint)는 제공하지 않습니다** — 토큰은 ainote 웹/계정 설정에서 발급한 키를 그대로 ChatGPT connector 의 Bearer 값으로 넣는 식. > Authorization Code flow / 토큰 자동 갱신이 필요한 케이스는 현재 미지원. 별도 OAuth provider 가 필요하면 GitHub issue 로 요청 주세요. ## 보안 권장 - 브리지는 `127.0.0.1` 만 listen (외부 노출 X) - 환경변수 키는 `~/.zshrc` 보다 `~/.zshenv` (gui app 도 읽음) - Tailscale 통해 다른 기기 ChatGPT 에서도 접근 가능 ## 한계 ChatGPT MCP 는 아직 **read-heavy** 작업에 적합: - ✅ list_tasks, get_dev_doc, list_dev_docs (읽기) - △ create_task, update_dev_doc (쓰기 — 매번 승인 화면) - ❌ 자동 워크플로우 (사용자 클릭 필요) 쓰기/자동화 많이 한다면 [Claude Code](/mcp/claude-code) 가 더 편함. ## 다음 - [3가지 transport 비교](/mcp/transports) - [Claude Code 연결](/mcp/claude-code) - [Telegram 연결](/mcp/telegram) (모바일에서 자동화) --- # Claude Code 연결 Claude Code (CLI) 에서 ainote 를 MCP 서버로 등록. ## 권장: hosted HTTP transport 가장 간단. 별도 설치 없음. `~/.claude.json` 편집: ```json { "mcpServers": { "ainote": { "type": "http", "url": "https://api.ainote.dev/api/mcp", "headers": { "Authorization": "McpKey YOUR_KEY_HERE" } } } } ``` 저장 후 Claude Code 재시작 (또는 `/mcp` 슬래시커맨드로 reload). ::: danger `type` 필드 누락 사고 주의 원격 MCP 서버는 반드시 `"type": "http"` (또는 `"sse"`) 명시. 빠지면 스키마 검증 실패로 `mcpServers` 블록 전체 미로드 — ainote 뿐 아니라 hyperbrowser, perplexity, render 등 **다른 모든 MCP 도 같이 안 됩니다**. 2026-04-15 이걸로 10개 동시 미로드 사고 발생. ::: ## 키 없이 시작 (가입 우선) 처음이라 키가 없으면 `headers` 빼고 등록: ```json { "mcpServers": { "ainote": { "type": "http", "url": "https://api.ainote.dev/api/mcp" } } } ``` Claude 에서: ``` ainote 가입 시켜줘 — 이메일 X / 비번 Y ``` 키 받은 뒤 `headers` 추가하고 재시작. ## 대안: stdio (로컬 npm 패키지) MCP 트래픽을 외부로 안 보내고 싶을 때: ```bash npm install -g @ainote/mcp ``` ```json { "mcpServers": { "ainote": { "command": "npx", "args": ["-y", "@ainote/mcp"], "env": { "AINOTE_API_URL": "https://api.ainote.dev", "AINOTE_API_KEY": "YOUR_KEY" } } } } ``` 이 경우에도 결국 `api.ainote.dev` 로 HTTPS 호출은 갑니다 — 차이는 stdio process 가 로컬에서 한 번 wrap 한다는 것뿐. ## 검증 Claude Code 안에서: ``` /mcp ``` `ainote` 가 connected 로 나오면 성공. 도구 목록 50개 이상 보임 — 정확한 개수는 계속 늘어나므로 고정값으로 인용하지 말 것([전체 카탈로그](/reference/) 참고). 테스트: ``` ainote 에 "테스트 태스크" 추가해줘 ``` ## Project-level 등록 (한 프로젝트만) `~/.claude.json` 대신 프로젝트 루트의 `.mcp.json`: ```json { "mcpServers": { "ainote": { "type": "http", "url": "https://api.ainote.dev/api/mcp", "headers": { "Authorization": "McpKey YOUR_KEY" } } } } ``` ⚠️ `.mcp.json` 은 git 에 커밋하지 마세요 — 키가 들어있음. ## Troubleshooting | 증상 | 원인 | 해결 | |------|------|------| | 도구 목록에 안 나옴 | type 필드 누락 | `"type": "http"` 추가 | | 401 Unauthorized | 키 누락/오타 | `Authorization` 헤더 확인 | | 다른 MCP 도 다 안 됨 | json schema 깨짐 | `~/.claude.json` JSON 검증 | | 시그널 끊김 | Render cold start | 재호출 시 정상 (5초 wait) | ## 다음 - 다른 클라이언트: [Claude Desktop](/mcp/claude-desktop) · [ChatGPT](/mcp/chatgpt) · [Cursor](/mcp/cursor) - [3가지 transport 비교](/mcp/transports) - [전체 도구 목록](/reference/) --- # Claude Desktop 연결 Claude Desktop (macOS / Windows 데스크톱 앱) 에서 ainote 사용. ## 권장: stdio 패키지 Claude Desktop 은 stdio transport 만 안정적으로 지원. ### 1. 패키지 설치 ```bash npm install -g @ainote/mcp ``` Node 18+ 필요. 미설치 시: ```bash brew install node # macOS # 또는 https://nodejs.org ``` ### 2. 설정 파일 편집 위치: - **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` - **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` ```json { "mcpServers": { "ainote": { "command": "npx", "args": ["-y", "@ainote/mcp"], "env": { "AINOTE_API_URL": "https://api.ainote.dev", "AINOTE_API_KEY": "h7Axq9XPsDTD2qr5yqtcCSaQ..." } } } } ``` ::: warning AINOTE_API_URL 도 같이 `@ainote/mcp` 의 빌트인 기본값은 아직 구 백엔드 (`ainote-5muq.onrender.com`) 를 가리킵니다. `env` 블록에 `AINOTE_API_URL` 을 같이 넣지 않으면 조용히 구 호스트로 붙습니다. ::: ### 3. Claude Desktop 재시작 완전 종료 (`Cmd+Q`) 후 다시 실행. ### 4. 검증 대화창 좌하단 🔌 아이콘 → "ainote" 가 connected 로 표시되어야 함. 테스트: ``` ainote 에 "데스크톱 테스트" 태스크 추가해줘 ``` ## 키 없이 시작 키만 빼고 등록 (`AINOTE_API_URL` 은 유지해서 신규 호스트로 가입되게): ```json { "mcpServers": { "ainote": { "command": "npx", "args": ["-y", "@ainote/mcp"], "env": { "AINOTE_API_URL": "https://api.ainote.dev" } } } } ``` 대화에서: ``` ainote 가입 시켜줘 — me@example.com / password123 ``` 키 받으면 `env` 추가 후 재시작. ## SSE 모드 (선택) Claude Desktop 일부 버전은 SSE 도 지원. ChatGPT 처럼 브리지 사용: ```bash export AINOTE_API_URL="https://api.ainote.dev" # 빌트인 default 는 구 onrender 호스트 export AINOTE_API_KEY="..." export AINOTE_MCP_HTTP_PORT=8765 # 기본 3030, env var 만 지원 (CLI flag 없음) ainote-mcp-http ``` 설정: ```json { "mcpServers": { "ainote": { "transport": { "type": "sse", "url": "http://localhost:8765/sse" } } } } ``` (브리지 프로세스는 항상 켜져 있어야 함) ## Troubleshooting ### "ainote" 가 안 뜸 체크: - JSON 문법: `cat ~/Library/Application\ Support/Claude/claude_desktop_config.json | jq .` - Node 설치: `which node` - 패키지 설치: `npm list -g @ainote/mcp` ### "command not found: npx" ```bash which npx # 없으면 Node 재설치 ``` 설정에서 `"command": "npx"` 대신 절대경로 사용 가능: ```json "command": "/opt/homebrew/bin/npx" ``` ### 401 Unauthorized env 의 `AINOTE_API_KEY` 확인. 키 prefix 안 붙음 — 그냥 64자 키만. 자세히: [Troubleshooting](/guide/troubleshooting). ## 다음 - [Claude Code 연결](/mcp/claude-code) - [3가지 transport 비교](/mcp/transports) - [전체 도구 목록](/reference/) --- # Cursor / Windsurf 연결 Cursor 와 Windsurf 는 **hosted HTTP** transport 를 1급 지원. Claude Code 와 거의 동일. ## Cursor ### 1. 설정 파일 `~/.cursor/mcp.json` (없으면 생성): ```json { "mcpServers": { "ainote": { "type": "http", "url": "https://api.ainote.dev/api/mcp", "headers": { "Authorization": "McpKey YOUR_KEY" } } } } ``` ### 2. Cursor 재시작 `Cmd+Shift+P` → "Cursor: Reload Window". ### 3. 검증 `Cmd+L` (Composer/Chat 패널) → "@" 입력 → ainote 도구 17개 보여야 함. 테스트: ``` @ainote create_task "Cursor 테스트" ``` ## Windsurf ### 1. 설정 파일 `~/.codeium/windsurf/mcp_config.json`: ```json { "mcpServers": { "ainote": { "type": "http", "url": "https://api.ainote.dev/api/mcp", "headers": { "Authorization": "McpKey YOUR_KEY" } } } } ``` ### 2. Windsurf 재시작 ### 3. Cascade 패널 → MCP 도구 사용 ## Project-level (한 프로젝트만) 두 IDE 모두 프로젝트 루트의 `.mcp.json` 또는 `.cursor/mcp.json` 지원: ```json { "mcpServers": { "ainote": { "type": "http", "url": "https://api.ainote.dev/api/mcp", "headers": { "Authorization": "McpKey TEAM_KEY" } } } } ``` ⚠️ git 에 커밋 X — `.gitignore` 에 추가: ``` .cursor/mcp.json .mcp.json ``` ## Cursor Rules / Windsurf Rules 연동 ainote 의 [Dev Docs](/memory/cursor-windsurf) 가 두 IDE 의 rules 파일을 중앙 관리. ### Cursor `.cursorrules` 동기화 ainote 에 등록: ``` .cursorrules 를 ainote 에 등록해줘 - title: "{project}-cursorrules" - category: "cursor" - local_path: "/Users/seunghan/{project}/.cursorrules" ``` 다른 기기에서: ``` ainote 에서 cursor rules 다 가져와 ``` → `pull_dev_docs --category cursor` → 모든 `.cursorrules` 복원. ### Windsurf `.windsurfrules` 도 동일 ``` category: "windsurf" local_path: ".windsurfrules" ``` ## 도구 호출 자동 승인 Cursor: `Cmd+,` → "MCP" → "Auto-approve tool calls" → ainote 의 read-only 도구만 자동 승인 권장: - ✅ `list_*`, `get_*`, `pull_*` 자동 - ⚠️ `create_*`, `update_*`, `delete_*` 수동 승인 ## Troubleshooting | 증상 | 해결 | |------|------| | 도구 안 뜸 | `type: "http"` 누락 확인 | | Cursor 재시작 후도 안 됨 | `~/.cursor/logs/` 에서 MCP 에러 확인 | | Windsurf cascade 에러 | `~/.codeium/windsurf/logs/` 확인 | ## 다음 - [Claude Code 연결](/mcp/claude-code) (거의 동일 패턴) - [Cursor / Windsurf rules 통합 관리](/memory/cursor-windsurf) - [3가지 transport 비교](/mcp/transports) --- # MCP 란? (30초 정리) **MCP (Model Context Protocol)** = Anthropic 이 만든 "AI ↔ 도구" 표준 프로토콜. ## 한 줄 비유 REST API 가 "프론트엔드 ↔ 백엔드" 표준이듯, **MCP 는 "AI 모델 ↔ 외부 도구" 표준**. ## ainote 입장에서 ainote 는 **MCP 서버** 입니다. 50개 이상의 도구를 노출 — 태스크, 메모, dev docs, vault, sync, 세션 핸드오프, 환경 동기화까지. 대표 예시: ``` create_task, update_task, delete_task, list_tasks, list_categories, create_dev_doc, update_dev_doc, delete_dev_doc, get_dev_doc, list_dev_docs, list_dev_categories, pull_dev_docs, handoff_save, handoff_list, handoff_get, vault_create, vault_clone, vault_sync, vault_list, vault_connect_status, sync_push, sync_pull, sync_list, signup_and_get_key, login_and_get_key, get_setup_guide ``` 전체 카탈로그(정확한 개수 포함)는 [도구 레퍼런스](/reference/) 참고 — 도구 수는 계속 늘어나므로 여기 숫자를 고정값으로 인용하지 말 것. MCP 클라이언트 (Claude Desktop, Claude Code, ChatGPT, Cursor, Telegram bot) 가 이 도구들을 호출 → ainote 가 실행 → 결과 반환. ## 작동 흐름 ``` [사용자] │ "내일 회의 추가해줘" ▼ [Claude / GPT] │ 도구 결정: create_task │ 파라미터 파싱: {content, due_date, ...} ▼ [MCP 클라이언트] │ JSON-RPC 2.0 over HTTP/stdio/SSE ▼ [ainote MCP 서버] │ Rails 8 API │ PostgreSQL insert │ Solid Queue 알림 스케줄 ▼ [응답] │ {"status": "ok", "task_id": 1234} ▼ [사용자에게 결과] "내일 10시 회의 추가했습니다 ✅" ``` ## 3가지 transport ainote 는 같은 도구 카탈로그를 **3가지 방식**으로 제공: | transport | 누가 쓰나 | 패키지 | |----------|---------|--------| | **stdio** | Claude Desktop, 보안 민감 | `npm i -g @ainote/mcp` | | **SSE** | ChatGPT, 일부 클라이언트 | `ainote-mcp-http` | | **hosted HTTP** | Claude Code, Cursor, 자동화 | `https://api.ainote.dev/api/mcp` | 자세히: [3가지 transport 비교](/mcp/transports). ## 다른 도구와 비교 | | ainote | filesystem | github | slack | |---|---|---|---|---| | 제공 | tasks/memory/vault | 파일 R/W | repo/PR | 채널/메시지 | | 인증 | API key | 로컬 | OAuth | OAuth | | transport | 3개 | stdio | stdio | http | | 로컬 데이터 | △ | ✅ | ❌ | ❌ | ## 더 알아보기 - 공식 스펙: - ainote 클라이언트별 가이드: - [Claude Desktop](/mcp/claude-desktop) - [Claude Code](/mcp/claude-code) - [ChatGPT (SSE)](/mcp/chatgpt) - [Cursor / Windsurf](/mcp/cursor) - [Telegram (Clawdbot)](/mcp/telegram) - [전체 도구 카탈로그](/reference/) --- # Telegram 연결 (Clawdbot) ainote 의 Telegram 봇 — 모바일에서 자연어로 태스크/메모 외부에서 추가. ## Clawdbot 이란 `@clawdbot` (Telegram) — ainote 의 Telegram interface. 내부적으로 [mcporter](https://github.com/clawdorg/mcporter) 가 MCP 도구들을 봇 명령으로 노출. ## 연동 절차 ### 1. 봇 시작 Telegram 에서 [@clawdbot](https://t.me/clawdbot) 검색 → "Start". 봇이 응답: ``` 👋 ainote Clawdbot 입니다. 계정 연동: /link ``` ### 2. 계정 연동 ``` /link ``` 봇이 응답: ``` 연동 코드: AB12-CD34-EF56 ainote.dev/settings/telegram 에서 입력하세요. 5분 유효. ``` ### 3. 코드 입력 → 코드 입력 → "연동" 클릭. 봇이 확인 메시지: ``` ✅ me@example.com 계정과 연동 완료 ``` ## 명령어 ### 태스크 ``` /task 내일 오전 10시 회의 준비 /today # 오늘 할 일 /week # 이번주 마감 /done #1234 # 완료 처리 /del #1234 # 삭제 ``` ### 메모리 ``` /memo 우리 팀은 PR review 24시간 내 SLA /notes feedback # feedback 타입 메모리만 /find "Stripe naming" # 검색 ``` ### 자유 질문 (AI 모드) ``` /ai 다음주 마감 중요한 거 알려줘 ``` → 내부적으로 Anthropic Claude API 호출 → `list_tasks` MCP → 응답. ## Inline 모드 다른 채팅에서 `@clawdbot` 입력 → 검색 → 결과 share: ``` @clawdbot 회의 ``` → "회의" 검색 결과를 친구 채팅에 공유 (본인만 보임). ## 알림 채널 ainote 의 태스크 알림이 Telegram 으로: 설정 → Telegram → "알림 받기" ON → 마감 임박 시 봇이 DM: ``` ⏰ 30분 후 마감 "강남역 미팅" — 오늘 10:00 [완료] [+30분 미루기] ``` 버튼 클릭으로 즉시 처리. ## 그룹 채팅 봇을 그룹에 초대: ``` /start@clawdbot /group_link # 이 그룹 → ainote 워크스페이스 연동 ``` → 그룹원 모두 같은 ainote workspace 접근 (계획됨, v0.5). ## 보안 - 연동 코드: 5분 단명, 1회 사용 - 봇 → ainote 호출은 user-scoped MCP key 사용 - DM 메시지 보관 안 함 (호출 후 즉시 폐기) - 그룹 모드: 메시지 본문 ainote 에 저장 안 됨 (명시적 `/memo` 만) ## Troubleshooting | 증상 | 해결 | |------|------| | `/link` 응답 없음 | 봇 차단 확인 → unblock | | 코드 만료 | 5분 지남 → `/link` 재실행 | | 알림 안 옴 | 설정에서 Telegram 알림 ON 확인 | | `/ai` 응답 느림 | Anthropic API 호출 — 5~10초 정상 | ## 한계 - 음성 메시지: 미지원 (계획됨, Whisper STT) - 파일 첨부: 미지원 - 인라인 키보드 일부 액션만 (전체 도구 노출 X) 전체 50+개 도구 쓰려면 Claude Code/Desktop 추천. ## 다음 - [태스크 개요](/tasks/overview) - [메모리 / Dev Docs](/memory/overview) - [3가지 transport 비교](/mcp/transports) --- # 3가지 transport 비교 ainote 는 같은 50+개 도구 카탈로그를 **3가지 transport** 로 제공. ## 한눈에 보기 | | stdio | SSE | hosted HTTP | |---|---|---|---| | **누가 쓰나** | Claude Desktop | ChatGPT | Claude Code, Cursor | | **설치** | `npm i -g @ainote/mcp` | `npx ainote-mcp-http` | 없음 | | **로컬 프로세스** | ✅ | ✅ (브리지) | ❌ | | **인증** | env var | env var | HTTP 헤더 | | **방화벽 친화** | ✅ outbound only | ✅ | ✅ | | **Cold start** | 즉시 | 즉시 | ~5초 (Render free) | | **AI 가 트래픽 검증** | 어려움 | SSE 로그 | curl 가능 | | **권장 시나리오** | 보안 민감 | ChatGPT 사용자 | 대부분 | ## 1. stdio — 로컬 wrap ``` [Claude Desktop] ──stdin/stdout──► [@ainote/mcp 프로세스] ──HTTPS──► [api.ainote.dev] ``` 설치: ```bash npm install -g @ainote/mcp ``` 설정 (`~/Library/Application Support/Claude/claude_desktop_config.json`): ```json { "mcpServers": { "ainote": { "command": "npx", "args": ["-y", "@ainote/mcp"], "env": { "AINOTE_API_URL": "https://api.ainote.dev", "AINOTE_API_KEY": "h7Axq9XPsDTD2qr5yqtcCSaQ..." } } } } ``` 특징: - Claude Desktop 만 stdio 지원 (구버전 호환) - 프로세스 별도 spawn → 살짝 느림 (~50ms overhead) - 외부에 보내는 트래픽이 한 단계 wrap 됨 (디버깅 어려움) ## 2. SSE — ChatGPT 브리지 ``` [ChatGPT] ──SSE──► [ainote-mcp-http 로컬 브리지] ──HTTPS──► [api.ainote.dev] ``` ChatGPT 의 MCP connector 가 SSE 만 지원. 로컬에서 브리지 실행: ```bash export AINOTE_API_URL="https://api.ainote.dev" # 빌트인 default 는 구 onrender 호스트 export AINOTE_API_KEY="..." export AINOTE_MCP_HTTP_PORT=8765 # 기본 3030, env var 만 지원 ainote-mcp-http ``` ChatGPT 설정: ``` URL: http://localhost:8765/sse Auth: (비워두기 — 브리지가 로컬 AINOTE_API_KEY env var 로 ainote 호출) ``` 특징: - 브라우저에서 ChatGPT 쓸 때만 작동 (브리지 켜둬야) - (선택) `AINOTE_ENABLE_OAUTH_AUTH=true` 로 띄우면 들어오는 `Authorization: Bearer …` 토큰 검증 모드로 동작 - ChatGPT 가 호출 단위로 승인 화면 띄움 자세히: [ChatGPT 연결](/mcp/chatgpt). ## 3. hosted HTTP — 가장 간단 ``` [Claude Code / Cursor] ──HTTPS──► [api.ainote.dev/api/mcp] ``` 설정 (`~/.claude.json`): ```json { "mcpServers": { "ainote": { "type": "http", "url": "https://api.ainote.dev/api/mcp", "headers": { "Authorization": "McpKey YOUR_KEY" } } } } ``` 특징: - ✅ 별도 프로세스 X - ✅ 가장 빠른 셋업 - ✅ curl 로 직접 테스트 가능 - ⚠️ Render free 인 경우 cold start 5초 이게 **대부분의 사용자에게 권장**. ## JSON-RPC 형식 (3개 모두 동일) ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "create_task", "arguments": { "content": "회의 준비", "due_date": "2026-05-08T10:00:00+09:00" } } } ``` 자세히: [JSON-RPC 호출 형식](/reference/json-rpc). ## 어떤 걸 골라야 하나 ``` ├─ Claude Code 사용? │ → hosted HTTP │ ├─ ChatGPT 사용? │ → SSE 브리지 │ ├─ Claude Desktop 만? │ → stdio │ ├─ Cursor / Windsurf? │ → hosted HTTP │ └─ 보안 민감 (트래픽 검증 필수)? → stdio (로컬에서 한 번 wrap) ``` ## 다음 - [Claude Desktop](/mcp/claude-desktop) - [Claude Code](/mcp/claude-code) - [ChatGPT (SSE)](/mcp/chatgpt) - [Cursor / Windsurf](/mcp/cursor) - [Telegram (Clawdbot)](/mcp/telegram) --- # 카테고리 / 폴더 구조 Dev Docs 의 `category` 는 ainote vault 의 디렉토리 결정. ## 표준 카테고리 | `category` | 디렉토리 | 용도 | |--------------|---------|------| | `claude` | `dev/claude/` | CLAUDE.md (Claude Code/Desktop) | | `cursor` | `dev/cursor/` | `.cursorrules`, `.cursor/rules/*.md` | | `windsurf` | `dev/windsurf/` | `.windsurfrules` | | `copilot` | `dev/copilot/` | `.github/copilot-instructions.md` | | `docs` | `dev/docs/` | README, ARCHITECTURE.md, 기타 | | `memory` | `memory/` | 토픽 메모리 (4 type) | ## 명명 규칙 ``` {project_name}-{purpose}.md ``` | 예시 | 의미 | |------|------| | `global-claude-guidelines.md` | 전역 (`~/CLAUDE.md`) | | `launchcrew-claude.md` | launchcrew 프로젝트 CLAUDE.md | | `tennis-bracket-cursor.md` | tennis_bracket .cursorrules | | `global-memory.md` | 전역 메모리 인덱스 | | `launchcrew-firebase.md` | launchcrew Firebase 메모 | | `personas-enhanced-activation.md` | SuperClaude PERSONAS.md | ## snake_case → kebab-case 프로젝트 디렉토리가 snake 인 경우 kebab 변환: ``` ~/tennis_bracket → tennis-bracket-claude.md ~/krx_listing → krx-listing-claude.md ~/krx_ai → krx-ai-claude.md ``` 이유: URL/파일명 가독성 + 일관성. ## 사용자 정의 카테고리 표준 외 카테고리도 가능: ```json { "title": "my-custom-rule.md", "category": "custom-rules", "local_path": "..." } ``` → vault 에 `dev/custom-rules/my-custom-rule.md` 자동 생성. `list_dev_categories` 가 동적으로 모든 사용 중인 카테고리 반환. ## 폴더 구조 권장 ### 옵션 A: 단순 (소수 프로젝트) ``` ainote vault ├── dev/ │ ├── claude/ │ └── cursor/ └── memory/ ``` ### 옵션 B: 프로젝트 우선 (다수 프로젝트, 17개+) ``` ainote vault ├── projects/ │ ├── launchcrew/ │ │ ├── CLAUDE.md │ │ ├── .cursorrules │ │ └── memory/ │ ├── tennis-bracket/ │ └── ... └── global/ ├── CLAUDE.md └── memory/ ``` → 단점: ainote 자동 카테고리화와 안 맞음, 수동 관리 필요. → 장점: 프로젝트별 한 폴더에서 모두 보임. [ainote-sync-redesign](https://github.com/seunghan91/ainote/blob/main/docs/sync-redesign.md) 가 후자 권장 (Phase 2 마이그레이션). ## 검색 ```bash # 카테고리별 ainote list_dev_docs '{"category":"claude"}' # 프로젝트별 (검색) ainote list_dev_docs '{"search":"launchcrew"}' # 카테고리 목록 ainote list_dev_categories '{}' ``` ## 다음 - [`list_dev_categories` API](/reference/list-dev-categories) - [메모리 4가지 타입](/memory/types) - [폴더 구조 권장 (sync 관점)](/sync/folder-structure) --- # CLAUDE.md 통합 관리 17개+ 프로젝트의 `CLAUDE.md` 를 ainote 한 곳에서. ## 문제 ``` ~/CLAUDE.md ← 전역 설정 ~/launchcrew/CLAUDE.md ← 프로젝트별 ~/tennis_bracket/CLAUDE.md ~/keeps/CLAUDE.md ... (17개 프로젝트) ``` 여러 노트북에서 작업 → 어디서 수정했는지 모름 → drift 발생. ## 해결 ainote 가 source of truth. ``` ainote vault └── dev/claude/ ├── global-claude-guidelines.md → ~/CLAUDE.md ├── launchcrew-claude.md → ~/launchcrew/CLAUDE.md ├── tennis-bracket-claude.md → ~/tennis_bracket/CLAUDE.md └── ... ``` 각 문서에 `local_path` 메타데이터 → `pull_dev_docs` 가 그 경로로 복원. ## 1회 등록 각 CLAUDE.md 마다: ``` ainote 에 ~/launchcrew/CLAUDE.md 등록해줘 - title: launchcrew-claude.md - category: claude-md - local_path: /Users/seunghan/launchcrew/CLAUDE.md ``` Claude 가 `create_dev_doc` 호출: ```json { "title": "launchcrew-claude.md", "category": "claude", "local_path": "/Users/seunghan/launchcrew/CLAUDE.md", "content": "" } ``` ## 일괄 등록 스크립트 ```bash #!/bin/bash # ~/scripts/ainote-claude-md-init.sh ainote-push() { local file="$1" local title="$2" local content content=$(python3 -c "import sys,json; print(json.dumps(open('$file').read()))") local local_path local_path=$(realpath "$file") ainote create_dev_doc "{ \"title\":\"$title\", \"content\":$content, \"subcategory\":\"claude-md\", \"local_path\":\"$local_path\" }" } # 전역 ainote-push ~/CLAUDE.md global-claude-guidelines.md # 모든 프로젝트 for proj in ~/launchcrew ~/tennis_bracket ~/keeps ~/triphelper ~/realpick ~/talkk; do if [ -f "$proj/CLAUDE.md" ]; then name=$(basename "$proj" | tr '_' '-') ainote-push "$proj/CLAUDE.md" "${name}-claude.md" fi done echo "✅ CLAUDE.md 일괄 등록 완료" ``` ## 수정 후 push CLAUDE.md 편집 후: ```bash ainote-push ~/launchcrew/CLAUDE.md launchcrew-claude.md # 이미 존재 → update_dev_doc (replace mode) ``` 또는 Claude 에서: ``` launchcrew CLAUDE.md 수정한 거 ainote 에 동기화해줘 ``` ## 새 기기 일괄 복원 ```bash ainote pull_dev_docs '{"category":"claude"}' ``` 응답: ``` ✓ /Users/seunghan/CLAUDE.md (created) ✓ /Users/seunghan/launchcrew/CLAUDE.md (updated) ✓ /Users/seunghan/tennis_bracket/CLAUDE.md (updated) ... (17 files) ``` ## 충돌 처리 두 기기에서 동시 편집 → 마지막 push 가 이김 (LWW). 복구: ```bash # git history 에서 손실된 버전 찾기 cd ~/.ainote-vault git log -p dev/claude/launchcrew-claude.md # 손실 버전 export git show :dev/claude/launchcrew-claude.md > /tmp/lost.md ``` 자세히: [sync 충돌](/sync/conflicts). ## 명명 컨벤션 ``` {project_name}-claude.md ``` - `project_name`: snake_case → kebab-case (`tennis_bracket` → `tennis-bracket`) - 전역: `global-claude-guidelines.md` - SuperClaude 설정: `personas-enhanced-activation.md` 등 자세히: [네임 규칙](/memory/categories). ## 다음 - [Cursor / Windsurf rules 같은 패턴](/memory/cursor-windsurf) - [4가지 메모리 타입](/memory/types) - [`pull_dev_docs` API](/reference/pull-dev-docs) --- # Cursor / Windsurf 룰 `.cursorrules`, `.windsurfrules`, `.github/copilot-instructions.md` 도 [CLAUDE.md 와 같은 패턴](/memory/claude-md) 으로 ainote 에 통합 관리. ## 카테고리 매핑 | 도구 | 파일 | ainote subcategory | |------|------|-------------------| | Claude Code/Desktop | `CLAUDE.md` | `claude` | | Cursor | `.cursorrules` | `cursor` | | Windsurf | `.windsurfrules` | `windsurf` | | GitHub Copilot | `.github/copilot-instructions.md` | `copilot` | | 일반 docs | `README.md`, `ARCHITECTURE.md` | `docs` | ## 등록 ### Cursor ``` ainote 에 ~/launchcrew/.cursorrules 등록해줘 - title: launchcrew-cursorrules - category: cursor - local_path: /Users/seunghan/launchcrew/.cursorrules ``` ### Windsurf 같은 패턴, `category: windsurf`, `local_path: ".../.windsurfrules"`. ### Copilot ``` title: launchcrew-copilot-instructions.md category: copilot local_path: /Users/seunghan/launchcrew/.github/copilot-instructions.md ``` ## 자동 디렉토리 ainote vault 에 자동 정리: ``` ainote vault └── dev/ ├── claude/ (CLAUDE.md 들) ├── cursor/ (.cursorrules 들) ├── windsurf/ (.windsurfrules) ├── copilot/ (copilot-instructions.md) └── docs/ (기타) ``` ## 카테고리별 일괄 pull 새 기기: ```bash # Cursor 룰만 ainote pull_dev_docs '{"category":"cursor"}' # Windsurf 만 ainote pull_dev_docs '{"category":"windsurf"}' # 전체 dev rules (claude + cursor + windsurf + copilot) ainote pull_dev_docs '{"category":"claude","cursor","windsurf","copilot"]}' # 모든 dev_docs ainote pull_dev_docs '{}' ``` ## 다중 도구 사용 시 권장 패턴 같은 프로젝트가 Claude + Cursor 둘 다 쓰면: **옵션 A: 같은 내용 양쪽**: - `CLAUDE.md` 와 `.cursorrules` 가 거의 동일 - ainote 에 별도 등록 (양쪽 sync) **옵션 B: 공통 내용 + symlink**: ```bash ln -s CLAUDE.md .cursorrules ``` → 한 파일 등록, 다른 도구도 자동 사용. **옵션 C: include 패턴** (Cursor/Windsurf 미지원): - Cursor 는 `@file` 으로 포함 가능 (Cursor 0.40+) - 그러면 `.cursorrules` 에 `@CLAUDE.md` 한 줄 ## Cursor Rules 스코프 Cursor 0.50+ 는 `.cursor/rules/*.md` 디렉토리 지원 — 여러 룰 파일. ``` ~/launchcrew/.cursor/rules/ ├── general.md ├── api-conventions.md └── testing.md ``` 각각 ainote 에: ``` title: launchcrew-cursor-rules-general.md category: cursor local_path: /Users/seunghan/launchcrew/.cursor/rules/general.md ``` ## 다음 - [CLAUDE.md 통합 관리](/memory/claude-md) - [메모리 카테고리 / 네이밍](/memory/categories) - [`pull_dev_docs` API](/reference/pull-dev-docs) --- # 메모리 / Dev Docs 개요 ainote 의 **두 번째 1급 시민** — Claude/Cursor/Windsurf 가 공유하는 마크다운 메모리. ## 두 가지 개념 ### 1. 토픽 메모리 (자유 형식) Claude 가 대화 중 자동으로 저장하는 작은 사실들. ainote 는 이를 [4가지 type](/memory/types) 으로 마크다운 frontmatter 에서 분류 — 단, **MCP 서버 자체가 type 필드를 색인하지는 않음**. 분류는 사용자/Claude 가 frontmatter 로 관리. | 타입 | 무엇 | 예시 | |------|------|------| | **user** | 사용자 정체성/선호 | "한국어로 응답, 파이썬 10년차" | | **feedback** | 작업 가이드 | "테스트 mock 금지 — 작년 prod 사고" | | **project** | 진행중 컨텍스트 | "2026-03-05 mobile freeze" | | **reference** | 외부 시스템 포인터 | "Linear 'INGEST' 프로젝트 = pipeline 버그" | ### 2. Dev Docs (구조화 문서) 프로젝트별 룰/가이드를 중앙 관리: - `claude` — `CLAUDE.md` - `cursor` — `.cursorrules`, `.cursor/rules/*.md` - `windsurf` — `.windsurfrules` - `copilot` — `.github/copilot-instructions.md` - `docs` — 일반 문서 서버는 모두 `category` 필드 하나로 구분 (디렉토리 분리). ## 왜 이걸 ainote 에 두나 **문제**: 17개 프로젝트의 CLAUDE.md 가 노트북마다 따로 놂. 맥미니에서 수정 → 맥북엔 반영 안 됨. **해결**: ainote 가 source of truth. - `create_dev_doc` — 새로 등록 (한 번만) - `update_dev_doc` — 변경 시 push - `pull_dev_docs` — 새 기기에서 한 방에 복원 (`local_path` 기준) ## MCP 도구 7개 | 도구 | 용도 | |------|------| | [`create_dev_doc`](/reference/create-dev-doc) | 새 문서 등록 | | [`update_dev_doc`](/reference/update-dev-doc) | 수정 (replace/append/prepend) | | [`delete_dev_doc`](/reference/delete-dev-doc) | Soft delete | | [`get_dev_doc`](/reference/get-dev-doc) | 단일 문서 조회 | | [`list_dev_docs`](/reference/list-dev-docs) | category + search 필터 | | [`list_dev_categories`](/reference/list-dev-categories) | 카테고리 목록 | | [`pull_dev_docs`](/reference/pull-dev-docs) | local_path 로 일괄 복원 | ## 자동 디렉토리 분류 `category` 자동으로 `dev/` 아래 정리: ``` ainote 클라우드 ├── dev/ │ ├── claude/ │ │ ├── global-claude-guidelines.md → ~/CLAUDE.md │ │ ├── tennis-bracket-claude.md → ~/tennis_bracket/CLAUDE.md │ │ └── launchcrew-claude.md → ~/launchcrew/CLAUDE.md │ ├── cursor/ │ ├── windsurf/ │ └── docs/ └── memory/ ├── global-MEMORY.md └── launchcrew-MEMORY.md ``` ## `local_path` — 복원의 핵심 문서 등록 시 `local_path` 항상 명시: ```json { "title": "tennis-bracket-claude.md", "category": "claude", "local_path": "/Users/seunghan/tennis_bracket/CLAUDE.md", "content": "..." } ``` `pull_dev_docs` 가 이 경로 기준으로 파일을 만듭니다. 없으면 복원 불가능. 크로스 플랫폼 경로 매핑 자동: macOS `~/...` ↔ WSL `~/...` ↔ Linux `~/...`. ## 메모리 저장 베스트 프랙티스 ### ✅ 저장할 것 - 사용자 가르쳐 준 사실 ("나는 X 회사 다님") - 코드에서 derive 안 되는 결정 근거 ("왜 mongo 대신 postgres") - 외부 시스템 포인터 (Slack 채널, Linear 프로젝트, 대시보드 URL) - 과거 사고 / 학습 ### ❌ 저장 X - 코드에서 보이는 것 (파일 구조, 함수 시그니처) - git log 로 알 수 있는 것 - 디버깅 결과 / 임시 상태 - CLAUDE.md 에 이미 있는 것 ## 다음 - [4가지 메모리 타입 자세히](/memory/types) - [CLAUDE.md 통합 관리](/memory/claude-md) - [Cursor / Windsurf 룰](/memory/cursor-windsurf) - [`create_dev_doc` API](/reference/create-dev-doc) --- # 4가지 메모리 타입 ainote 의 토픽 메모리는 **`type`** 필드로 분류. Claude 가 자동으로 적절한 타입 선택. ## 1. `user` — 사용자 정체성 **언제**: 사용자의 역할/선호/지식/배경 학습 시. **예시**: ```markdown --- name: user-role description: 사용자 역할 type: user --- 사용자는 한국거래소 8년차 개발자. Rails 메인, Flutter 보조. 한국어로 응답 선호. 기술 용어는 영어 그대로. ``` **활용**: 모든 응답이 사용자 배경에 맞춰 톤 조절. ## 2. `feedback` — 작업 가이드 **언제**: 사용자가 "이렇게 하지 마" 또는 "이렇게 해" 라고 가르쳐줄 때. **구조**: ```markdown --- name: feedback-no-mock-tests type: feedback --- 통합 테스트는 실제 DB 사용, mock 금지. **Why**: 작년 mock test 통과했지만 prod 마이그레이션 깨짐. **How to apply**: integration test 작성 시 항상 testcontainers 또는 transactional fixtures. ``` **규칙**: 단순 규칙 + Why + How to apply 구조 권장. ## 3. `project` — 진행중 컨텍스트 **언제**: 누가 무엇을 왜 언제까지 하나 학습 시. **중요**: 시간 빨리 변하므로 상대 날짜 → 절대 날짜 변환 (`목요일` → `2026-05-07`). **예시**: ```markdown --- name: project-mobile-freeze type: project --- 2026-05-15 부터 mobile 팀 release branch cut → non-critical merge freeze. **Why**: iOS 7.0 출시 코드 안정화. **How to apply**: 그 날짜 이후 mobile-related PR 은 mobile lead 승인 받고 진행. ``` **TTL**: 프로젝트 종료 시 정리 권장. 오래된 project 메모리는 misleading. ## 4. `reference` — 외부 시스템 포인터 **언제**: 사용자가 외부 시스템 위치 알려줄 때 (Slack 채널, Linear 프로젝트, Grafana URL). **예시**: ```markdown --- name: reference-grafana-api-latency type: reference --- oncall API 레이턴시 대시보드: grafana.internal/d/api-latency **용도**: request handling 코드 수정 시 페이지 발생 가능 → 변경 전 baseline 캡처. ``` **활용**: 사용자가 "그 대시보드" 언급할 때 Claude 가 찾아서 링크. ## 자동 분류 Claude 가 대화에서 메모리 저장 시 type 자동 결정: | 사용자 발화 | 추론 type | |-----------|----------| | "나는 X 회사 다닌다" | `user` | | "이거 하지 마, 이전에 망함" | `feedback` | | "다음주 X 마감이라 ..." | `project` | | "Linear INGEST 프로젝트에 ..." | `reference` | ## 검색 / 필터 ```bash ainote list_dev_docs '{"type":"feedback"}' ainote list_dev_docs '{"type":"project","search":"mobile"}' ``` ## 메모리 정리 `project` 타입은 정기 정리 권장: ```bash # 30일 이상 안 쓴 project 메모리 후보 ainote list_dev_docs '{"type":"project","not_accessed_within_days":30}' ``` 수동 검토 후 delete. `reference` 와 `user` 는 거의 영구. `feedback` 은 코드/도구 변경 시 stale 됨 → 분기마다 검토. ## 다음 - [메모리 / Dev Docs 개요](/memory/overview) - [CLAUDE.md 통합 관리](/memory/claude-md) - [`create_dev_doc` API](/reference/create-dev-doc) --- # Backend Completeness Principle > **"백엔드는 철저하게, 사용자는 간단하게."** > > Backend is thorough; user-facing surface is simple. ainote 의 모든 설계 결정은 이 한 줄을 따른다. 본 문서는 이 원칙이 무엇이고, 왜 **agent-native** 노트 백엔드의 토대가 되는지 설명한다. --- ## 1. 두 개의 레이어, 서로 다른 책임 ### Backend (시스템 관리 레이어) **책임**: 상태 관리, 추적, 감사(audit), 검증. **원칙**: - 모든 상태 변화를 기록한다 — `created_at`, `reviewed_at`, `reviewed_by`, `auto_approved` 같은 메타데이터 전부 - 완전한 비즈니스 로직 — 자동수락 규칙, 상태 머신, 엣지 케이스 처리 - 모든 정보를 API 로 제공 — 상태 조회, 상세 정보, 히스토리, 감사 로그 - 향후 분석 / 감사 / 문제 추적에 필요한 모든 데이터 보존 ### User-Facing Layer (사용자 인터페이스 + 에이전트) **책임**: 사용자 경험, 의사결정 지원. **원칙**: - 최소한의 정보 제공 — 의사결정에 필요한 것만 - 빠른 행동 유도 — 버튼 2-3개 이내 - 낮은 인지 부하 — 사용자가 시스템 내부를 알 필요 없음 --- ## 2. 구체 예시 — ainote 의 도구들 ### 예 1: `handoff_save` / `handoff_list` / `handoff_get` **사용자(에이전트) 가 보는 것**: ```ruby handoff_save({project: "logi", topic: "phase4", time: "1555", content: "..."}) # → "✅ handoff saved to handoffs/logi-phase4-1555-2026-05-14.txt" ``` **백엔드가 한 일**: - vault 의 `file_indices` 테이블에 path / body / git_sha / updated_at 갱신 - 같은 호출이지만 7일 이전 핸드오프들을 **opportunistic purge** — 별도 cron 없이 write 트래픽에 cleanup 분산 - VaultIndexer 로 git commit (Render Singapore + 사용자 GitHub repo) - subscriber 알림 (Phase 2 에서 SSE notification) **사용자는 purge 도, git commit 도, 미래 subscriber 도 모른다**. 그런데 에이전트가 `handoff_list` 가 destructive 임을 알아야 자율 호출을 차단할 수 있다 → 그래서 우리는 `destructiveHint: true` annotation 으로 advertise. ### 예 2: `vault_sync(action: "push")` **사용자가 결정하는 것**: `conflictResolution: merge | overwrite | abort` 한 가지. **백엔드가 처리하는 것**: - 파일 hash 비교 → 충돌 감지 - 충돌 시 conflictResolution 정책 적용 → merge 면 양 버전 보존, overwrite 면 remote 덮어쓰기, abort 면 트랜잭션 롤백 - `reindex_path!` 가 빈 content 받으면 indexed file 삭제 가능 (그래서 보수적으로 `destructiveHint: true`) - 디바이스별 sync 상태 업데이트 (multi-device 동기화 무결성) - Git commit + remote push ### 예 3: `login_and_get_key` **사용자가 요청**: 이메일 + 비밀번호 → MCP key. **백엔드의 silent side effect**: 이 사용자가 아직 MCP key 가 없으면 **자동 생성**한다 (`user.mcp_keys.first || user.mcp_keys.create!`). 이게 왜 중요한가? 자율 에이전트가 이 도구를 read-only 로 잘못 인식하면 **사용자 동의 없이 비밀 키 생성** 행위가 일어난다. 그래서 우리는 `readOnlyHint: false` 로 명확히 표기 — 에이전트 runtime 이 게이트할 수 있게. --- ## 3. 왜 이것이 agent-native 의 토대인가 전통적 API 디자인에서는 backend complexity 와 frontend simplicity 사이의 간극을 **사용자 문서** 가 메운다. 사용자가 README 를 읽고 side effect 를 학습한다. **에이전트는 README 를 안 읽는다**. `tools/list` 만 본다. 따라서: 1. **모든 side effect 를 annotations 로 advertise** — `readOnlyHint` / `destructiveHint` / `idempotentHint` / `openWorldHint`. README 가 아니라 wire contract 에 박아둔다 2. **모든 destructive operation 을 보수적으로 표기** — 의심스러우면 destructive=true. 자율 에이전트가 false negative (destructive 한데 read-only 로 잘못 인식) 보다 false positive (read-only 인데 destructive 로 잘못 인식) 가 안전 3. **에이전트가 알아야 할 것만 노출** — 백엔드의 purge cron, git commit, conflict resolution 알고리즘 같은 건 도구 설명에 안 들어간다. 단지 **결과** 와 **side effect class** 만 이 분리가 ainote 가 50+개 도구를 동시에 Claude · Cursor · Windsurf · OpenAI · LangChain · AutoGen 에 노출할 수 있게 한 토대다. 도구 정의를 단순하게 유지하면서도 백엔드는 production-grade 의 복잡도를 가진다. --- ## 4. 출처와 더 깊은 참고 이 원칙은 우리의 다른 프로젝트 **CartaG** 에서 처음 명문화됐다. CartaG 의 disclosure_requests 시스템 (3자 익명 정보 공개 요청) 에서: ``` Backend (DB & API) ├─ disclosure_requests 테이블 │ ├─ id, post_id, requester_id │ ├─ status: pending/approved/rejected/blocked │ ├─ created_at, reviewed_at, reviewed_by │ ├─ access_token, token_expires_at │ ├─ rejection_reason, auto_approved │ └─ (모든 상태 추적) Frontend (UI) └─ DM 메시지 (간단) ├─ Park Min Ho 2024.12.15 ├─ "어제 여의도에서..." ├─ [승인] [거절] ← 이것만! └─ (상태 정보 노출 X) ``` 사용자는 "어제 여의도에서..." 한 문장 + [승인] [거절] 버튼만 본다. 백엔드는 access_token / token_expires_at / rejection_reason / auto_approved 같은 메타데이터 전부를 추적한다. ainote 가 같은 원칙을 에이전트 인터페이스에 적용한 것이다. --- ## 5. 안티패턴 이 원칙이 깨지는 상황: ❌ **UI / 도구 출력에 복잡한 상태 정보 노출** — "transaction_id: txn_abc123, audit_log_id: log_xyz789 ..." 같은 응답은 사용자도 에이전트도 의사결정에 못 쓴다. ❌ **백엔드에서 "사용자 편의" 라는 이유로 데이터 축약** — `handoff_list` 응답에서 git_sha 를 빼면, 백엔드의 추적 무결성이 깨진다. 표시 안 하면 됐지 데이터를 버리지 마라. ❌ **프론트엔드/에이전트에서 상태 추적 로직 구현** — 클라이언트가 "이 핸드오프가 며칠 됐으니 곧 purge 되겠다" 같은 계산 하면 안 된다. 백엔드의 진실에 의존하라. ✅ **양쪽이 독립적으로 동작** — 백엔드: 모든 정보를 완전하게 API 로 제공 / 프론트엔드: 필요한 것만 UI 에 표시. --- ## 6. Trade-off 의 명시적 선택 복잡도 vs 사용성: | 측면 | 우리의 선택 | |------|------------| | 최종 사용자 | ✅ 매우 단순 · ✅ 빠른 액션 · ❌ 상태 추적 정보 없음 (필요 없음) | | 자율 에이전트 | ✅ tool annotations 로 충분한 메타 · ✅ destructive call 게이트 가능 · ❌ 백엔드 내부 로직 불투명 (그래야 함) | | 개발자 / 시스템 | ✅ 완전한 복잡도 · ✅ 모든 상태 추적 · ❌ 복잡함 (필요한 복잡함) | **결정 근거**: 사용자는 빠른 의사결정이 우선. 에이전트는 신뢰 가능한 hint 가 우선. 시스템은 완전한 추적이 우선. 셋이 충돌하면 분리해서 해결. --- ## 7. 적용 체크리스트 — 새 도구 추가 시 새 MCP 도구를 추가할 때 다음을 자문하라: 1. 이 도구가 호출되면 백엔드에서 **어떤 side effect** 가 일어나는가? (DB write, file write, 외부 API 호출, cleanup job, audit log) 2. 그 중 사용자/에이전트가 **알아야** 할 것은? (annotations 로 표현) 3. 사용자/에이전트가 **결정해야** 할 것은? (input schema 파라미터) 4. 그 외 모든 것은 백엔드가 알아서 한다 — 문서/도구 응답에 노출하지 마라 5. 보수적 표기: 의심스러우면 `destructiveHint: true`, `openWorldHint: true` → [실제 매핑 표](/reference/annotations) · [도구 추가 가이드](/contributing) ---

이 원칙은 ainote 의 모든 도구 / 모든 API / 모든 UI 결정에 적용된다.
새 도구를 추가하기 전 이 페이지를 한 번 더 읽으면 우리 모두에게 도움이 된다.

--- # 인증 헤더 ## 기본 형식 ```http Authorization: McpKey ``` 또는 (호환): ```http Authorization: Bearer ``` (`` 자리에 실제 키 값) ## 키 종류 | 키 | 길이 | 헤더 prefix | |---|------|-----------| | User API Key | 24 | `McpKey` 또는 `Bearer` | | MCP Key | 64 | `McpKey` 또는 `Bearer` | 자세히: [API Key 인증](/guide/auth). ## 인증 없이 호출 가능 도구 (예외 3개) `tools/list` + 온보딩 2개: - `tools/list` — 도구 카탈로그 조회 - `signup_and_get_key` - `login_and_get_key` - `get_setup_guide` 이 4개는 헤더 없이 200 응답. 다른 도구 헤더 없이 호출 시: ```json { "error": { "code": -32001, "message": "Unauthorized: missing or invalid Authorization header" } } ``` ## 키 마스킹 응답에서 키가 표시되는 경우 마지막 4자만: ``` Mcp Key (created): h7Ax...QWERTY (last 4 chars shown after first display) ``` 전체 키는 발급 직후에만 한 번 표시. ## 사용량 헤더 응답에 사용량 정보: ```http X-RateLimit-Limit: 120 X-RateLimit-Remaining: 87 X-RateLimit-Reset: 1746662400 # Unix timestamp X-Usage-Today: 1234 X-Usage-Today-Limit: 20000 ``` ## CORS 브라우저 직접 호출 (셀프호스팅 등): ```http Access-Control-Allow-Origin: https://your-domain.example.com Access-Control-Allow-Headers: Content-Type, Authorization ``` 기본 `api.ainote.dev` 는 `*` 안 함 → 백엔드 또는 MCP 클라이언트 통해. ## SSO / OAuth (계획됨) v0.5+: - Google / Apple SSO 로 키 발급 - OAuth 2.0 flow (PKCE) - 단명 access token + refresh token ## 다음 - [API Key 인증 가이드](/guide/auth) - [JSON-RPC 형식](/reference/json-rpc) - [에러 코드](/reference/errors) --- # 변경 로그 semver 비슷하지만 v1 이전이라 minor breaking 가능. ## v0.x (현재 — alpha) ### 0.4 (2026-05 예정) - ✨ Vault — git backend (5 도구) - ✨ Sync — HLC + LWW (3 도구) - ✨ ChatGPT MCP connector (SSE 브리지) - 🔄 dev_doc 4가지 메모리 type 자동 분류 - 🐛 type 필드 누락 시 에러 메시지 개선 ### 0.3 (2026-04) - ✨ Telegram bot (Clawdbot) 연동 - ✨ MCP 키 사용량 통계 (per-key) - ✨ Streaming API (NDJSON, list_tasks 1000+) - 🔄 자연어 파싱 개선 (한국어 시간 표현) - 🐛 반복 태스크 timezone 버그 ### 0.2 (2026-03) - ✨ `signup_and_get_key`, `login_and_get_key` (계정 없이 시작) - ✨ Cursor / Windsurf 통합 (rules 동기화) - ✨ Web Push (PWA 알림) - 🔄 Solid Queue 도입 (Sidekiq → SQ) ### 0.1 (2026-02) - 🎉 첫 공개 - 태스크 CRUD + 18 필터 - Dev Docs (CLAUDE.md 동기화) - Claude Desktop / Code 지원 ## v1 로드맵 (2026-Q4 예정) - API stability 보장 (SemVer 시작) - OAuth 2.0 + Google/Apple SSO - Vault sharing (collaborator) - 모바일 위젯 - Search MCP 도구 (의미 기반) ## Breaking Changes ### 0.3 → 0.4 - `update_dev_doc` 의 `mode` 기본값 `replace` (이전 `append`) - `vault_*` 도구 추가 (기존 호출 영향 X) ### 0.2 → 0.3 - `create_task` 의 `due` → `due_date` 로 rename (`title` 도 `content` 로 함께 통일) - `list_tasks` 응답에 `format` 파라미터 추가 (기본 `json`) ## Migration 가이드 `/migrations/{from}-to-{to}.md` 페이지 (계획됨, 0.5+). ## 다음 - [API 레퍼런스 개요](/reference/) - [GitHub releases](https://github.com/seunghan91/ainote/releases) --- # create_dev_doc 새 dev_doc / 메모리 등록. ## 시그니처 ```json { "name": "create_dev_doc", "arguments": { "title": "launchcrew-claude.md", "content": "", "category": "claude", "local_path": "/Users/seunghan/launchcrew/CLAUDE.md" } } ``` ## 파라미터 | 파라미터 | 타입 | 필수 | 설명 | |---------|------|------|------| | `title` | string | ✅ | 파일명 (`.md` 확장자 포함) | | `content` | string | ✅ | 마크다운 / json / yaml 본문 | | `category` | string | | 서브카테고리 (`claude`, `cursor`, `windsurf`, `copilot`, `docs`). 기본 `docs` | | `content_type` | string | | `markdown` / `json` / `yaml` / `text` (title 확장자에서 자동 감지) | | `local_path` | string | | 로컬 절대경로 ([`pull_dev_docs`](/reference/pull-dev-docs) 복원 대상) | ::: tip 카테고리 이름 서버 필드는 `category` 입니다 (`subcategory` 아님). 카테고리 값 자체는 "subcategory" 라고 부르기도 합니다 — 헷갈리지 마세요. 도구 인자는 항상 **`category`**. ::: ## 응답 ```json { "content": [ { "type": "text", "text": "✅ Saved: launchcrew-claude.md → dev/claude/launchcrew-claude.md" } ] } ``` ## local_path 중요성 `local_path` 없으면 [`pull_dev_docs`](/reference/pull-dev-docs) 가 파일을 만들 수 없음. 권장: 절대 경로. `~` 으로 시작하는 경로는 자동으로 canonicalize 됨 (서버 저장 시 `$HOME` → `~`). ## 에러 | 코드 | 메시지 | |------|-------| | -32602 | `title is required` | | -32602 | `content is required` | | -32603 | `Title already exists in this category` → [`update_dev_doc`](/reference/update-dev-doc) 사용 | ## Claude 자연어 ``` ~/launchcrew/CLAUDE.md 를 ainote 에 등록해줘 — title launchcrew-claude.md, category claude ``` ## 다음 - [`update_dev_doc`](/reference/update-dev-doc) - [`pull_dev_docs`](/reference/pull-dev-docs) - [메모리 / Dev Docs 개요](/memory/overview) --- # create_task 새 태스크 생성. 필수 필드: `content`. ## 시그니처 ```json { "name": "create_task", "arguments": { "content": "강남역 미팅 준비", "due_date": "2026-05-08T10:00:00+09:00", "is_important": true, "category_id": "uuid-here", "location": "강남역 3번 출구 스타벅스", "notification_minutes_before": 30 } } ``` ## 파라미터 | 파라미터 | 타입 | 필수 | 설명 | |---------|------|------|------| | `content` | string | ✅ | 태스크 내용 (자연어 OK) | | `due_date` | string (ISO) | | 마감일 (예: `2026-04-21T15:00:00+09:00`) | | `due_time` | string `HH:MM` | | 시간만 (`due_date` 에 시간 없을 때) | | `start_date` | string (ISO) | | 다일 이벤트 시작일 | | `is_all_day` | boolean | | 종일 일정 | | `is_important` | boolean | | 중요 표시 | | `category_id` | string (UUID) | | 카테고리 ID (`list_categories` 로 조회) | | `notes` | string | | 자유 메모 / 상세 | | `location` | string | | 사람용 위치 (예: "Starbucks Gangnam") | | `location_lat` | number | | GPS 위도 | | `location_lng` | number | | GPS 경도 | | `travel_time` | number | | 마감 N분 전 이동시간 (알림 스케줄링) | | `repeat_rule` | string | | 반복 규칙 (`daily`, `weekly`, `monthly` 또는 RRULE) | | `notification_minutes_before` | number | | 마감 N분 전 알림 (`due_date` 필요) | ## 응답 ```json { "content": [ { "type": "text", "text": "✅ 태스크 생성됨 — \"강남역 미팅 준비\" (내일 10:00)" } ] } ``` ## Claude 자연어 ``` "내일 오전 10시 강남역 미팅 준비, 30분 전 알림" 추가 ``` → Claude 가 자연어를 파싱해서 `due_date`, `location`, `notification_minutes_before` 추출 후 호출. ## 에러 | 코드 | 메시지 | 원인 | |------|-------|------| | -32602 | `Task content is required` | `content` 누락 | | -32602 | `Invalid due_date format` | ISO 8601 아님 | | -32602 | `Category not found or not accessible` | `category_id` 잘못 | ## 다음 - [`update_task`](/reference/update-task) - [`list_tasks`](/reference/list-tasks) - [태스크 개요](/tasks/overview) --- # delete_dev_doc dev_doc soft delete. ## 시그니처 ```json { "name": "delete_dev_doc", "arguments": { "title": "old-notes.md" } } ``` 또는: ```json { "id": "uuid-here" } ``` ## 파라미터 | 파라미터 | 타입 | 설명 | |---------|------|------| | `title` | string | 문서 title (id 와 둘 중 하나) | | `id` | string | UUID | | `category` | string | 동명 문서 disambiguation | ## 응답 ```json { "content": [ { "type": "text", "text": "🗑️ Deleted: old-notes.md" } ] } ``` ## Soft delete 서버 DB 에 `deleted_at` 마킹. 30일 후 영구 정리. 웹 UI 에서 즉시 영구 삭제 가능. ## 에러 | 코드 | 메시지 | |------|-------| | -32602 | `title or id is required` | | -32603 | `Document not found` | ## Claude ``` ainote 에서 "old-notes.md" 삭제 ``` ## 다음 - [`create_dev_doc`](/reference/create-dev-doc) - [데이터 내보내기 / 삭제](/guide/data-export) --- # delete_task 태스크 삭제 — soft delete (서버에서 30일 후 영구 정리). ## 시그니처 ```json { "name": "delete_task", "arguments": { "id": "uuid-here" } } ``` ## 파라미터 | 파라미터 | 타입 | 필수 | 설명 | |---------|------|------|------| | `id` | string | ✅ | 태스크 ID | ## 응답 ```json { "content": [ { "type": "text", "text": "✅ 태스크 삭제됨" } ] } ``` ## Soft delete 동작 - DB 에 `deleted_at` 마킹 → 모든 `list_tasks` 응답에서 제외 - 서버의 `TaskCleanupJob` (매일 새벽 2시) 이 30일 지난 항목 영구 삭제 - 영구 삭제 시 cascade: notifications, notification_schedules, paper_tasks, recurring_instances ## 즉시 영구 삭제 MCP 도구는 soft delete 만 노출. 즉시 영구 삭제는 웹 UI: ## 에러 | 코드 | 메시지 | |------|-------| | -32602 | `id is required` | | -32602 | `Task not found` | ## Claude ``` 태스크 #uuid 삭제 ``` ## 다음 - [`update_task`](/reference/update-task) — 완료 처리는 update_task 사용 - [태스크 개요](/tasks/overview) --- # 에러 코드 JSON-RPC 표준 + ainote 확장. ## JSON-RPC 표준 | 코드 | 의미 | 원인 | |------|------|------| | -32700 | Parse error | JSON 문법 오류 | | -32600 | Invalid Request | jsonrpc 필드 누락 등 | | -32601 | Method not found | `method` 가 `tools/list` / `tools/call` 아님 | | -32602 | Invalid params | 도구 파라미터 잘못 | | -32603 | Internal error | 서버 내부 오류 | ## ainote 확장 (-32000 ~ -32099) | 코드 | 의미 | 원인 | 해결 | |------|------|------|------| | -32000 | Tool not found | `params.name` 이 도구 목록에 없음 | `tools/list` 로 확인 | | -32001 | Unauthorized | 헤더 누락 / 잘못 | [인증 가이드](/guide/auth) | | -32002 | Rate limit exceeded | 분당/일당 한도 초과 | `Retry-After` 헤더 대기 | | -32003 | Validation failed | 비즈니스 규칙 위반 | `data.field` 확인 | | -32004 | Quota exceeded | 저장량/vault 개수 한도 | 정리 또는 유료 플랜 | | -32005 | Resource not found | 태스크/문서 ID 없음 | `list_*` 로 확인 | | -32006 | Conflict | 충돌 (예: vault sync) | 수동 해결 | | -32007 | Permission denied | 키 권한 부족 (read-only 키로 write) | 권한 키 발급 | ## 응답 예시 ```json { "jsonrpc": "2.0", "id": 1, "error": { "code": -32602, "message": "Invalid params: content is required", "data": { "field": "content", "constraint": "required" } } } ``` ## HTTP 상태 코드 JSON-RPC 응답은 보통 HTTP 200 (에러도). 단: - HTTP 401 — `Authorization` 자체 형식 잘못 (JSON 파싱도 안 함) - HTTP 429 — Rate limit (매우 빠른 reject) - HTTP 500 — 서버 다운 (보통은 200 + error code) ## 자주 보는 시나리오 ### 처음 호출이 401 ```json { "code": -32001, "message": "Unauthorized" } ``` 체크: - 키 prefix `McpKey ` (공백 1개) - 키 64자 (`MCP Key`) 또는 24자 (`User API Key`) - 키 발급 후 폐기 안 됨 ### 자연어 파싱 실패 ```json { "code": -32602, "message": "Could not parse due_date from content", "data": { "content": "tomorrow at noonish maybe" } } ``` 해결: 명시적 ISO 8601: ```json { "content": "회의", "due_date": "2026-05-08T12:00:00+09:00" } ``` ### 429 Rate limit ```http HTTP/1.1 429 Too Many Requests Retry-After: 30 { "error": { "code": -32002, "message": "Rate limit exceeded" } } ``` `Retry-After: 30` → 30초 대기 후 재시도. ### Quota 초과 ```json { "code": -32004, "message": "Vault quota exceeded", "data": { "current": 100, "limit": 100, "type": "vault_count" } } ``` 해결: 안 쓰는 vault 삭제 또는 유료 플랜. ## 클라이언트 처리 권장 ```typescript async function callMcp(name, args) { const res = await fetch(URL, {...}); const json = await res.json(); if (json.error) { if (json.error.code === -32002) { // Rate limit — exponential backoff const retry = parseInt(res.headers.get('Retry-After') || '5'); await sleep(retry * 1000); return callMcp(name, args); } if (json.error.code === -32001) { throw new AuthError(json.error.message); } throw new McpError(json.error.code, json.error.message, json.error.data); } return json.result; } ``` ## 다음 - [JSON-RPC 형식](/reference/json-rpc) - [Troubleshooting](/guide/troubleshooting) --- # get_dev_doc 단일 dev_doc 조회 (전체 본문 포함). ## 시그니처 ```json { "name": "get_dev_doc", "arguments": { "title": "launchcrew-claude.md" } } ``` 또는 UUID: ```json { "id": "uuid-here" } ``` ## 파라미터 | 파라미터 | 타입 | 설명 | |---------|------|------| | `title` | string | 문서 title (id 와 둘 중 하나) | | `id` | string | UUID | | `category` | string | 동명 문서 disambiguation | | `include_versions` | boolean | 버전 히스토리 포함 (기본 false) | ## 응답 ```json { "content": [ { "type": "text", "text": "[Formatted doc info + content]" }, { "type": "resource", "resource": { "uri": "ainote://dev_docs/uuid", "mimeType": "application/json", "text": "{\"id\":\"uuid\",\"title\":\"...\",\"content\":\"...\",\"category\":\"claude\",\"local_path\":\"...\"}" } } ] } ``` `include_versions: true` 시 응답에 version history 추가. ## 에러 | 코드 | 메시지 | |------|-------| | -32602 | `title or id is required` | | -32603 | `Document not found` | ## Claude ``` launchcrew-claude.md 보여줘 launchcrew-claude.md 변경 이력 (include_versions: true) ``` ## 다음 - [`list_dev_docs`](/reference/list-dev-docs) — 검색 + 필터 - [`update_dev_doc`](/reference/update-dev-doc) --- # API 레퍼런스 ainote MCP 서버의 전체 도구 카탈로그. ## 공통 사항 - **Endpoint**: `https://api.ainote.dev/api/mcp` - **Protocol**: JSON-RPC 2.0 over HTTP POST - **Auth**: `Authorization: McpKey ` 헤더 - **Content-Type**: `application/json` 자세히: [JSON-RPC 호출 형식](/reference/json-rpc) · [인증](/reference/auth) · [에러 코드](/reference/errors). ## 도구 카탈로그 (50개 이상) > 정확한 개수는 계속 늘어나므로 고정값으로 인용하지 말 것. 아래 표는 주요 도구군 기준 정리. ### 온보딩 (3) | 도구 | 인증 필요 | 설명 | |------|----------|------| | [`signup_and_get_key`](/reference/signup) | ❌ | 가입 + MCP 키 발급 | | [`login_and_get_key`](/reference/login) | ❌ | 기존 계정 로그인 + 키 발급 | | [`get_setup_guide`](/reference/setup-guide) | ❌ | 클라이언트별 설정 가이드 | ### 태스크 (5) | 도구 | 설명 | |------|------| | [`create_task`](/reference/create-task) | 새 태스크 (자연어 파싱 포함) | | [`update_task`](/reference/update-task) | 수정 / 완료 처리 | | [`delete_task`](/reference/delete-task) | 삭제 (soft, 30일) | | [`list_tasks`](/reference/list-tasks) | 18+ 필터 조회 | | [`list_categories`](/reference/list-categories) | 카테고리 목록 | ### 메모리 / Dev Docs (7) | 도구 | 설명 | |------|------| | [`create_dev_doc`](/reference/create-dev-doc) | 새 문서 등록 | | [`update_dev_doc`](/reference/update-dev-doc) | 수정 (replace/append/prepend) | | [`delete_dev_doc`](/reference/delete-dev-doc) | 삭제 | | [`get_dev_doc`](/reference/get-dev-doc) | 단일 조회 | | [`list_dev_docs`](/reference/list-dev-docs) | 검색 + 필터 | | [`list_dev_categories`](/reference/list-dev-categories) | 카테고리 목록 | | [`pull_dev_docs`](/reference/pull-dev-docs) | local_path 일괄 복원 | ### Vault (5) | 도구 | 설명 | |------|------| | [`vault_create`](/reference/vault-create) | 새 vault | | [`vault_clone`](/reference/vault-clone) | git clone (다른 기기) | | [`vault_sync`](/reference/vault-sync) | pull + push | | [`vault_list`](/reference/vault-list) | vault 목록 | | [`vault_connect_status`](/reference/vault-connect-status) | git 연결 상태 | ### Sync (3) | 도구 | 설명 | |------|------| | [`sync_push`](/reference/sync-push) | 로컬 파일 → ainote | | [`sync_pull`](/reference/sync-pull) | ainote → 로컬 (since 기준) | | [`sync_list`](/reference/sync-list) | sync 대상 목록 | ## Rate Limits | 키 종류 | 분당 | 시간당 | 일당 | |--------|------|-------|------| | User API Key | 60 | 1,000 | 10,000 | | MCP Key (free) | 120 | 2,000 | 20,000 | | MCP Key (paid) | 600 | 10,000 | 무제한 | 429 응답 시 `Retry-After` 헤더 확인. ## SDK / 라이브러리 | 언어 | 패키지 | |------|--------| | Node.js (MCP) | [`@ainote/mcp`](https://www.npmjs.com/package/@ainote/mcp) | | Ruby (Rails 통합) | [`@seunghan/rails-api-client`](https://www.npmjs.com/package/@seunghan/rails-api-client) | | 직접 호출 | [JSON-RPC 형식](/reference/json-rpc) | ## OpenAPI `https://api.ainote.dev/openapi.yaml` (계획됨, v0.x). ## 변경 로그 [변경 로그](/reference/changelog) 참고. --- # JSON-RPC 호출 형식 ainote 의 모든 MCP 도구는 [JSON-RPC 2.0](https://www.jsonrpc.org/specification) over HTTP POST. ## Endpoint ``` POST https://api.ainote.dev/api/mcp Content-Type: application/json Authorization: McpKey ``` ## 요청 형식 ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "<도구 이름>", "arguments": { ... } } } ``` | 필드 | 타입 | 설명 | |------|------|------| | `jsonrpc` | string | 항상 `"2.0"` | | `id` | int / string | 요청 식별자 (응답에 echo) | | `method` | string | `tools/list` 또는 `tools/call` | | `params.name` | string | 도구 이름 | | `params.arguments` | object | 도구별 파라미터 | ## 응답 형식 (성공) ```json { "jsonrpc": "2.0", "id": 1, "result": { "content": [ { "type": "text", "text": "..." } ] } } ``` 또는 도구가 structured 반환: ```json { "result": { "content": [{ "type": "text", "text": "..." }], "structuredContent": { "task_id": 1234, "created_at": "2026-05-07T10:00:00Z" } } } ``` ## 응답 형식 (에러) ```json { "jsonrpc": "2.0", "id": 1, "error": { "code": -32602, "message": "Invalid params: content is required", "data": { "field": "content" } } } ``` 자세히: [에러 코드](/reference/errors). ## 도구 목록 조회 ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/list" } ``` 응답: ```json { "result": { "tools": [ { "name": "create_task", "description": "Create a new task with natural language parsing.", "inputSchema": { "type": "object", "properties": { ... } } }, ... ] } } ``` ## curl 예시 ```bash curl -X POST https://api.ainote.dev/api/mcp \ -H "Content-Type: application/json" \ -H "Authorization: McpKey YOUR_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_tasks", "arguments": { "due_today": true } } }' ``` shell 함수로 wrap: [shell 함수](/cli/shell-function). ## Streaming (NDJSON) 대량 응답 시 `Accept: application/x-ndjson` 헤더 → 각 결과 한 줄씩: ```bash curl -N -X POST https://api.ainote.dev/api/mcp \ -H "Authorization: McpKey YOUR_KEY" \ -H "Accept: application/x-ndjson" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_tasks","arguments":{"limit":1000}}}' ``` ## Batch 요청 여러 도구 한번에: ```json [ { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_tasks", "arguments": {} } }, { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "list_categories", "arguments": {} } } ] ``` 응답도 배열 (순서 보장 X — `id` 로 매칭). ## 다음 - [인증 헤더](/reference/auth) - [에러 코드](/reference/errors) - [전체 도구 카탈로그](/reference/) --- # list_categories 태스크 카테고리 목록. ## 시그니처 ```json { "name": "list_categories", "arguments": {} } ``` 파라미터 없음. ## 응답 ```json { "content": [ { "type": "text", "text": "📂 카테고리 목록\n- 업무 (12)\n- 개인 (5)\n..." }, { "type": "resource", "resource": { "uri": "ainote://categories/list", "mimeType": "application/json", "text": "{\"categories\":[{\"id\":\"uuid\",\"name\":\"업무\",\"color\":\"#0088ff\"}]}" } } ] } ``` ## Claude ``` 카테고리 목록 "업무" 카테고리 ID ``` ## 다음 - [태스크 카테고리 자세히](/tasks/categories) - [`list_tasks`](/reference/list-tasks) --- # list_dev_categories dev/ 아래 사용 중인 모든 카테고리 목록. ## 시그니처 ```json { "name": "list_dev_categories", "arguments": {} } ``` 파라미터 없음. ## 응답 ```json { "content": [ { "type": "text", "text": "📂 카테고리\n- claude (17 docs)\n- cursor (5)\n..." }, { "type": "resource", "resource": { "uri": "ainote://dev_categories/list", "mimeType": "application/json", "text": "{\"categories\":[{\"name\":\"claude\",\"count\":17},...]}" } } ] } ``` 표준 + 사용자 정의 카테고리 모두 포함. ## 표준 카테고리 (예시) | `category` | 디렉토리 | 용도 | |--------------|---------|------| | `claude` | `dev/claude/` | CLAUDE.md (Claude Code/Desktop) | | `cursor` | `dev/cursor/` | `.cursorrules`, `.cursor/rules/*.md` | | `windsurf` | `dev/windsurf/` | `.windsurfrules` | | `copilot` | `dev/copilot/` | `.github/copilot-instructions.md` | | `docs` | `dev/docs/` | README, ARCHITECTURE.md, 기타 | | `memory` | `memory/` | 토픽 메모리 | ## 사용 ``` 어떤 카테고리들이 있는지 보여줘 ``` ## 다음 - [`list_dev_docs`](/reference/list-dev-docs) - [메모리 카테고리 / 네이밍](/memory/categories) --- # list_dev_docs dev_docs 검색 + 필터. ## 시그니처 ```json { "name": "list_dev_docs", "arguments": { "category": "claude", "search": "firebase" } } ``` ## 파라미터 | 파라미터 | 타입 | 설명 | |---------|------|------| | `category` | string | 서브카테고리 (`claude`, `cursor`, `windsurf`, `copilot`, `docs`, `memory`, ...) | | `search` | string | title 키워드 | | `content_type` | string | `markdown` / `json` / `yaml` / `text` | ::: tip 단순한 필터 이 도구는 위 3개 필터만 지원합니다. 메모리 type 별 필터, 날짜 범위, 페이징 등은 현재 없음 (계획됨). ::: ## 응답 ```json { "content": [ { "type": "text", "text": "[Formatted list]" }, { "type": "resource", "resource": { "uri": "ainote://dev_docs/list", "mimeType": "application/json", "text": "{\"docs\":[{\"id\":\"uuid\",\"title\":\"...\",\"category\":\"claude\",\"local_path\":\"...\"}]}" } } ] } ``` `content` 본문은 응답에 미포함 (요약만) — 본문은 [`get_dev_doc`](/reference/get-dev-doc) 으로. ## 자주 쓰는 호출 ```jsonc // 모든 CLAUDE.md { "category": "claude" } // 모든 cursor 룰 { "category": "cursor" } // 키워드 검색 (전체) { "search": "firebase" } // 카테고리 + 키워드 { "category": "claude", "search": "stripe" } // 모든 dev_docs { } ``` ## 에러 | 코드 | 메시지 | |------|-------| | -32602 | `Invalid content_type` | ## Claude ``` 모든 CLAUDE.md 보여줘 firebase 들어간 dev_doc 검색 ``` ## 다음 - [`get_dev_doc`](/reference/get-dev-doc) - [`pull_dev_docs`](/reference/pull-dev-docs) - [메모리 카테고리 / 네이밍](/memory/categories) --- # list_tasks 태스크 조회 — 자연어 + 18개 필터 지원. ## 시그니처 ```json { "name": "list_tasks", "arguments": { "status": "pending", "due_today": true, "is_important": true, "limit": 20 } } ``` ## 파라미터 전체 ### 상태 | 파라미터 | 값 | |---------|-----| | `status` | `pending` / `completed` | | `is_important` | boolean | | `overdue` | boolean — 마감 지난 미완료 | | `due_today` | boolean — 오늘 마감 | | `has_notification` | boolean | ### 검색 / 위치 | 파라미터 | 설명 | |---------|------| | `search` | 내용 키워드 | | `location` | 위치 부분일치 (예: `"여의도"`, `"서울"`) | | `category_id` | 카테고리 UUID | ### 날짜 범위 | 파라미터 | 설명 | |---------|------| | `due_date_start` | ISO 8601, `due_date >= 이 값` | | `due_date_end` | `due_date <= 이 값` | | `completed_date_start` | 완료일 >= | | `completed_date_end` | 완료일 <= | | `created_date_start` | 생성일 >= | | `created_date_end` | 생성일 <= | ### 정렬 / 페이징 | 파라미터 | 값 | 기본 | |---------|-----|------| | `sort_by` | `due_date` / `created_at` / `completed_at` / `updated_at` / `is_important` | `created_at` | | `sort_order` | `asc` / `desc` | `desc` | | `limit` | 1~500 | 25 | ## 자연어 호출 예시 | 발화 | 파라미터 | |------|---------| | "오늘 할 일" | `{ due_today: true }` | | "이번 주 마감" | `{ due_date_start: "<월요일>", due_date_end: "<일요일>" }` | | "마감 지난 미완료" | `{ overdue: true, sort_by: "due_date", sort_order: "asc" }` | | "여의도에서 중요한 미완료" | `{ location: "여의도", is_important: true, status: "pending" }` | | "지난달 완료 업무 카테고리" | `{ category_id: "...", status: "completed", completed_date_start: "...", completed_date_end: "..." }` | ## 응답 ```json { "content": [ { "type": "text", "text": "[Formatted task list]" }, { "type": "resource", "resource": { "uri": "ainote://tasks/list", "mimeType": "application/json", "text": "{\"tasks\":[...]}" } } ] } ``` structured data 는 `content[1].resource.text` 의 JSON 에 포함. ## 에러 | 코드 | 메시지 | |------|-------| | -32602 | `Invalid date format (expected ISO 8601)` | | -32602 | `Invalid sort_by value` | ## Claude ``` 오늘 할 일 보여줘 이번 주 마감 중요한 거 강남에 있는 미완료 태스크 ``` ## 다음 - [필터링 자세히 (오늘/이번주 등 시간 계산)](/tasks/filtering) - [`create_task`](/reference/create-task) - [태스크 개요](/tasks/overview) --- # login_and_get_key 기존 계정 로그인 + 새 MCP 키 발급. **인증 헤더 불필요**. ## 시그니처 ```json { "name": "login_and_get_key", "arguments": { "email": "me@example.com", "password": "SuperSecret123" } } ``` ## 파라미터 | 파라미터 | 타입 | 필수 | 설명 | |---------|------|------|------| | `email` | string | ✅ | 가입한 이메일 | | `password` | string | ✅ | 비밀번호 | ## 응답 ```json { "content": [ { "type": "text", "text": "✅ 로그인 완료\nemail: me@example.com\nMCP Key: h7Ax... (full key shown once)" } ] } ``` 기존 키는 그대로 유지 (폐기 안 함). 같은 사용자가 여러 키 가능. ## 에러 | 코드 | 메시지 | 원인 | |------|-------|------| | -32603 | `Invalid email or password` | | | -32002 | `Too many login attempts` | 잠금 | ## Claude 에서 자연어 ``` ainote 로그인 — me@example.com / SuperSecret123 ``` ## 새 기기에서 키 발급 패턴 새 노트북에서 처음 사용할 때: 1. MCP 등록 (헤더 없이) 2. `login_and_get_key` 호출 → 새 키 3. 헤더에 키 추가 후 reload ## 다음 - [`signup_and_get_key`](/reference/signup) - [API Key 인증 가이드](/guide/auth) --- # pull_dev_docs dev_docs 일괄 복원 — `local_path` 기준으로 로컬 파일 생성/덮어쓰기. ## 시그니처 ```json { "name": "pull_dev_docs", "arguments": { "category": "claude" } } ``` ## 파라미터 | 파라미터 | 타입 | 설명 | |---------|------|------| | `category` | string | 단일 카테고리 (생략 시 모든 dev_docs) | ::: tip 단순한 필터 이 도구는 `category` 만 받습니다. `since`, `dry_run`, `force` 같은 옵션은 현재 없음 — 항상 모든 매칭 doc 을 받아 로컬에 덮어씀. ::: ## 동작 1. 매칭되는 모든 `dev_doc` 중 `local_path` 가 set 된 것만 가져옴 2. 현재 플랫폼 자동 감지 (macOS / WSL / Linux / Windows) 3. 경로 매핑 (예: `~/...` ↔ `/mnt/c/Users/...`) 4. 부모 디렉토리 자동 mkdir 5. 각 파일 작성 ## 크로스 플랫폼 경로 매핑 자동: - macOS `~/...` ↔ WSL `~/...` ↔ Linux `~/...` - Claude project keys 매핑 (예: `-Users-seunghan` ↔ `-mnt-c-Users-Owner`) 새 기기 (다른 OS) 셋업 시 `pull_dev_docs '{}'` 한 번이면 알아서 적절한 경로로. ## 응답 ```json { "content": [ { "type": "text", "text": "🖥️ Platform: darwin | Home: /Users/seunghan | Project key: -Users-seunghan\n\nWritten (17):\n✅ launchcrew-claude.md → /Users/seunghan/launchcrew/CLAUDE.md\n✅ tennis-bracket-claude.md → /Users/seunghan/tennis_bracket/CLAUDE.md\n...\n\nSkipped (no local_path): some-doc.md, ..." } ] } ``` ## skipped 이유 - `local_path` 없음 (등록 시 미설정) - 디렉토리 권한 부족 → errors 에 기록 ## 에러 모음 응답에 부분 실패 표시 (`Errors:` 섹션): ``` Errors: ❌ x.md: EACCES: permission denied, mkdir '/restricted' ``` 전체 호출 자체는 보통 성공 — 개별 파일 실패는 응답에 누적. ## 시나리오 ### 새 기기 셋업 ```json {} ``` → 모든 dev_docs (모든 카테고리) 일괄. ### 카테고리만 ```json { "category": "claude" } { "category": "cursor" } ``` ## Claude ``` 모든 CLAUDE.md 다시 가져와 ainote 에서 cursor 룰 다 받아 ``` ## 다음 - [`get_dev_doc`](/reference/get-dev-doc) - [CLAUDE.md 통합 관리](/memory/claude-md) - [새 디바이스 셋업 예시](/examples/new-device) --- # get_setup_guide 클라이언트별 설정 가이드 반환. **인증 헤더 불필요**. ## 시그니처 ```json { "name": "get_setup_guide", "arguments": { "client": "claude-code", "language": "ko" } } ``` | 파라미터 | 값 | 설명 | |---------|-----|------| | `client` | `claude-code` / `claude-desktop` / `chatgpt` / `cursor` / `windsurf` / `telegram` | 클라이언트 | | `language` | `ko` / `en` | 응답 언어 (기본 `en`) | ## 응답 ```json { "client": "claude-code", "language": "ko", "guide": "# Claude Code 등록\n\n1. ~/.claude.json 편집...", "config_template": { "mcpServers": { "ainote": { "type": "http", "url": "https://api.ainote.dev/api/mcp", "headers": { "Authorization": "McpKey " } } } }, "docs_url": "https://docs.ainote.dev/mcp/claude-code" } ``` ## 사용 시나리오 Claude 에서: ``` ainote setup 가이드 보여줘 — Claude Code 용 ``` → 가이드 + 설정 JSON 즉시 받음 → 사용자 클립보드 복사. ## 클라이언트 우선 (계획됨) 자동 감지: ```json { "arguments": {} } ``` → User-Agent 기반 추론 → 적절한 가이드 반환. ## 다음 - [Claude Code 연결](/mcp/claude-code) — 사람용 가이드 - [3가지 transport 비교](/mcp/transports) --- # signup_and_get_key 신규 가입 + MCP 키 발급. **인증 헤더 불필요**. ## 시그니처 ```json { "name": "signup_and_get_key", "arguments": { "email": "me@example.com", "password": "SuperSecret123", "name": "Seunghan" } } ``` ## 파라미터 | 파라미터 | 타입 | 필수 | 설명 | |---------|------|------|------| | `email` | string | ✅ | 유효한 이메일 | | `password` | string | ✅ | 최소 6자 | | `name` | string | | 표시 이름 (선택) | ## 응답 ```json { "content": [ { "type": "text", "text": "✅ 가입 완료\nemail: me@example.com\nMCP Key: h7Ax... (full key shown once)" } ] } ``` ⚠️ MCP Key 는 **이번에만 한 번** 표시. 즉시 저장. ## 에러 | 코드 | 메시지 | 원인 | |------|-------|------| | -32602 | `email is required` | | | -32602 | `password must be at least 6 characters` | 6자 미만 | | -32603 | `email already taken` | → [`login_and_get_key`](/reference/login) 사용 | ## 보안 - 비번: bcrypt 저장 - HTTPS only - IP/User-Agent 로그 ## Claude 에서 자연어 ``` ainote 가입 시켜줘 — 이메일 me@example.com / 비밀번호 SuperSecret123 / 이름 승한 ``` ## 다음 - [`login_and_get_key`](/reference/login) - [API Key 인증 가이드](/guide/auth) --- # sync_list ::: tip ✅ 라이브 (서버) `vault_*` / `sync_*` 도구는 `api.ainote.dev` 서버에서 **동작합니다** — JSON-RPC `POST /api/mcp` 또는 [`@ainote/sdk`](/build/sdk) (`ai.vault.*` / `ai.sync.*`)로 호출하세요. (일부 vault 도구는 연결된 git-backed vault가 필요. 참고: `@ainote/mcp` npm 패키지 구버전엔 번들이 안 됐을 수 있으니 직접 JSON-RPC 또는 SDK 사용 권장.) ::: ainote hub 의 sync 대상 파일 메타데이터 조회 (content 없이). ## 시그니처 ```json { "name": "sync_list", "arguments": { "prefix": "global/", "since": "2026-05-01T00:00:00Z" } } ``` | 파라미터 | 타입 | 설명 | |---------|------|------| | `prefix` | string | 경로 prefix (디렉토리 필터) | | `since` | ISO 8601 | 변경 시각 필터 | | `device_id` | string | 자기 디바이스 변경 제외 | | `limit` | int | 기본 1000 | ## 응답 ```json { "files": [ { "path": "global/CLAUDE.md", "size_bytes": 12000, "sha256": "abc123...", "hlc": "2026-05-07T14:01:00.0.macmini", "git_sha": "def456...", "device_id": "macmini-2026-04", "stored_at": "2026-05-07T14:01:01Z" }, ... ], "total_count": 53, "total_size_bytes": 234000 } ``` content 없음 — 받으려면 `sync_pull` 호출. ## 사용 시나리오 ### sync 전 diff 확인 ```python local_files = scan_local() remote_files = mcp_call("sync_list", {}) diffs = compare(local_files, remote_files) print(f"{len(diffs)} files differ") ``` ### 디렉토리별 통계 ```bash ainote sync_list '{"prefix":"global/"}' ainote sync_list '{"prefix":"projects/launchcrew/"}' ``` ### 최근 변경 모니터 ```bash # 매 시간 ainote sync_list '{"since":"<1h ago>"}' ``` ## 에러 | 코드 | 메시지 | |------|-------| | -32602 | invalid prefix | | -32602 | invalid since format | ## Claude ``` 지난주 변경된 sync 파일 보여줘 launchcrew 폴더에 뭐 있나 ``` ## 다음 - [`sync_push`](/reference/sync-push) - [`sync_pull`](/reference/sync-pull) - [동기화 개요](/sync/overview) --- # sync_pull ::: tip ✅ 라이브 (서버) `vault_*` / `sync_*` 도구는 `api.ainote.dev` 서버에서 **동작합니다** — JSON-RPC `POST /api/mcp` 또는 [`@ainote/sdk`](/build/sdk) (`ai.vault.*` / `ai.sync.*`)로 호출하세요. (일부 vault 도구는 연결된 git-backed vault가 필요. 참고: `@ainote/mcp` npm 패키지 구버전엔 번들이 안 됐을 수 있으니 직접 JSON-RPC 또는 SDK 사용 권장.) ::: ainote hub → 로컬. 변경된 파일 받기. ## 시그니처 ### 단일 파일 ```json { "name": "sync_pull", "arguments": { "path": "global/CLAUDE.md" } } ``` ### 여러 파일 (since) ```json { "name": "sync_pull", "arguments": { "since": "2026-05-06T00:00:00Z", "device_id": "macmini-2026-04-A1B2" } } ``` ### 초기 (모든 파일) ```json { "name": "sync_pull", "arguments": { "initial": true, "device_id": "macmini-2026-04-A1B2" } } ``` | 파라미터 | 타입 | 설명 | |---------|------|------| | `path` | string | 단일 파일 | | `since` | ISO 8601 | 이 시각 이후 변경된 것 | | `initial` | boolean | 모든 파일 (새 디바이스) | | `device_id` | string | 자기 디바이스 변경은 제외 | | `paths` | array | 특정 경로들만 | ## 응답 ### 단일 ```json { "path": "global/CLAUDE.md", "content": "", "sha256": "abc123...", "hlc": "2026-05-07T14:02:30.0.macbook", "git_sha": "def456...", "stored_at": "2026-05-07T14:02:30Z", "device_id": "macbook-2026-03", "remote_mtime": "2026-05-07T14:02:30Z" } ``` ### Multi (since / initial) ```json { "files": [ { "path": "global/CLAUDE.md", "content": "...", "sha256": "...", "hlc": "..." }, { "path": "global/PERSONAS.md", "content": "...", ... } ], "total_count": 53, "total_size_bytes": 234000 } ``` ## Streaming 큰 결과 (`since` 가 오래됨, `initial`) 는 NDJSON 자동: ```bash curl -N -X POST .../api/mcp \ -H "Accept: application/x-ndjson" \ -d '{"...":"sync_pull","arguments":{"initial":true}}' ``` 각 줄이 한 파일. ## 에러 | 코드 | 메시지 | |------|-------| | -32005 | path not found | | -32602 | invalid since format | ## Claude ``` 어제 이후 변경된 거 다 받아 ``` ```bash ainote sync_pull '{"since":"2026-05-06T00:00:00Z"}' ``` ## 다음 - [`sync_push`](/reference/sync-push) - [`sync_list`](/reference/sync-list) - [sync-now 알고리즘](/sync/algorithm) --- # sync_push ::: tip ✅ 라이브 (서버) `sync_push`/`sync_*`/`vault_*` 도구는 `api.ainote.dev` 서버에서 **동작합니다** — JSON-RPC `POST /api/mcp` 또는 [`@ainote/sdk`](/build/sdk)(`ai.sync.*`)로 호출하세요. (참고: `@ainote/mcp` npm 패키지의 구버전엔 번들이 안 됐을 수 있으니, 직접 JSON-RPC 또는 SDK 사용을 권장합니다.) ::: 로컬 파일 → ainote hub. [sync 시스템](/sync/overview) 의 일부. ## 시그니처 ```json { "name": "sync_push", "arguments": { "path": "global/CLAUDE.md", "content": "", "sha256": "abc123...", "hlc": "2026-05-07T14:01:00.000Z.0.macmini", "device_id": "macmini-2026-04-A1B2" } } ``` | 파라미터 | 타입 | 필수 | 설명 | |---------|------|------|------| | `path` | string | ✅ | ainote vault 내 경로 | | `content` | text | ✅ | 파일 본문 | | `sha256` | string | ✅ | content 의 SHA-256 (서버 검증) | | `hlc` | string | ✅ | Hybrid Logical Clock | | `device_id` | string | ✅ | 송신 디바이스 식별 | | `mtime` | ISO 8601 | | 로컬 mtime (충돌 임계 비교용) | ## 응답 ```json { "path": "global/CLAUDE.md", "git_sha": "def456...", "hlc": "2026-05-07T14:01:00.000Z.0.macmini", "previous_hlc": "2026-05-06T...", "size_bytes": 12345, "stored_at": "2026-05-07T14:01:01Z" } ``` ## 충돌 서버가 더 새로운 HLC 가지고 있으면: ```json { "error": { "code": -32006, "message": "Conflict: server has newer HLC", "data": { "your_hlc": "2026-05-07T14:01:00.0.macmini", "server_hlc": "2026-05-07T14:02:30.0.macbook", "time_diff_seconds": 90 } } } ``` → 클라이언트가 `sync-now.sh` 알고리즘에 따라 처리 ([conflicts](/sync/conflicts)). ## sha256 검증 서버가 받은 content 의 sha256 을 계산해서 클라이언트가 보낸 것과 비교. 다르면: ```json { "error": { "code": -32602, "message": "sha256 mismatch" } } ``` → 전송 중 손상 가능성 → 재시도. ## 에러 | 코드 | 메시지 | |------|-------| | -32602 | invalid path | | -32602 | sha256 mismatch | | -32006 | conflict | | -32004 | quota exceeded (5 GB) | ## 다음 - [`sync_pull`](/reference/sync-pull) - [`sync_list`](/reference/sync-list) - [sync-now 알고리즘](/sync/algorithm) --- # update_dev_doc 기존 dev_doc 수정. ## 시그니처 ```json { "name": "update_dev_doc", "arguments": { "title": "launchcrew-claude.md", "content": "", "mode": "replace" } } ``` ## 파라미터 | 파라미터 | 타입 | 필수 | 설명 | |---------|------|------|------| | `content` | string | ✅ | 새 본문 (mode 따라 처리) | | `title` | string | △ | title 또는 id 둘 중 하나 필요 | | `id` | string | △ | 문서 UUID | | `category` | string | | 동명 문서 disambiguation | | `mode` | string | | `replace` (기본) / `append` / `prepend` | | `local_path` | string | | 로컬 경로 갱신 | ## mode 비교 | mode | 동작 | 권장 | |------|------|------| | `replace` | 본문 전체 덮어씀 | ✅ 일반 | | `append` | 끝에 추가 | 메모 누적 | | `prepend` | 앞에 추가 | 긴급 공지 | `append`/`prepend` 는 로컬 파일과 drift 위험 → 가능하면 `replace`. ## 응답 ```json { "content": [ { "type": "text", "text": "✅ Updated: launchcrew-claude.md (mode: replace)" } ] } ``` ## 에러 | 코드 | 메시지 | |------|-------| | -32602 | `content is required` | | -32602 | `title or id is required` | | -32603 | `Document not found` | ## Claude ``` launchcrew-claude.md 를 새 내용으로 교체 launchcrew-claude.md 끝에 "추가 메모" 붙여 ``` ## 다음 - [`create_dev_doc`](/reference/create-dev-doc) - [`delete_dev_doc`](/reference/delete-dev-doc) --- # update_task 기존 태스크 수정. `id` 필수, 변경할 필드만 지정. ## 시그니처 ```json { "name": "update_task", "arguments": { "id": "uuid-here", "completed_at": "2026-05-08T10:35:00+09:00" } } ``` ## 파라미터 | 파라미터 | 타입 | 설명 | |---------|------|------| | `id` | string | ✅ 태스크 ID | | `completed_at` | string (ISO) / null | 완료 처리 (ISO 시각) 또는 `null` 로 미완료 복귀 | | `content` | string | 내용 변경 | | `is_important` | boolean | 중요 토글 | | `due_date` | string (ISO) | 마감 변경 | | `due_time` | string `HH:MM` | | | `start_date` | string (ISO) | | | `is_all_day` | boolean | | | `category_id` | string (UUID) | | | `notes` | string | | | `location` / `location_lat` / `location_lng` | | | | `travel_time` | number | | | `repeat_rule` | string | | | `notification_minutes_before` | number / null | `null` 전달 시 기존 알림 제거 | 지정하지 않은 필드는 그대로 유지 (PATCH 방식). ## 완료 처리 패턴 ```json { "id": "uuid", "completed_at": "2026-05-08T10:35:00+09:00" } ``` 미완료 복귀: ```json { "id": "uuid", "completed_at": null } ``` ## 응답 ```json { "content": [ { "type": "text", "text": "✅ 태스크 업데이트됨" } ] } ``` ## 에러 | 코드 | 메시지 | |------|-------| | -32602 | `id is required` | | -32602 | `Task not found` | | -32602 | `Invalid completed_at format` | ## Claude ``` 태스크 #uuid 완료 처리해줘 태스크 #uuid 마감 내일 오후 3시로 변경 ``` ## 다음 - [`create_task`](/reference/create-task) - [`delete_task`](/reference/delete-task) - [태스크 개요](/tasks/overview) --- # vault_clone ::: tip ✅ 라이브 (서버) `vault_*` / `sync_*` 도구는 `api.ainote.dev` 서버에서 **동작합니다** — JSON-RPC `POST /api/mcp` 또는 [`@ainote/sdk`](/build/sdk) (`ai.vault.*` / `ai.sync.*`)로 호출하세요. (일부 vault 도구는 연결된 git-backed vault가 필요. 참고: `@ainote/mcp` npm 패키지 구버전엔 번들이 안 됐을 수 있으니 직접 JSON-RPC 또는 SDK 사용 권장.) ::: vault 의 GitHub clone URL 을 회수한다. ainote 는 clone 을 대신 수행하지 않고 URL 과 git 명령만 반환한다. 자세한 가이드: [vault/clone](/vault/clone). ## 시그니처 ```json { "name": "vault_clone", "arguments": { "name": "personal", "target_path": "~/vaults/personal" } } ``` | 파라미터 | 타입 | 필수 | 설명 | |---------|------|------|------| | `name` | string | ✅ | vault 이름 또는 slug | | `target_path` | string | | 반환 안내에 쓸 로컬 clone 경로 (선호값) | ## 응답 ```json { "github_repo_full_name": "me/personal", "clone_url_https": "https://github.com/me/personal.git", "clone_url_ssh": "git@github.com:me/personal.git", "target_path": "~/vaults/personal", "command": "git clone https://github.com/me/personal.git ~/vaults/personal" } ``` 인증은 사용자의 GitHub 자격증명(gh / SSH key / PAT)으로 한다. ainote MCP 키는 관여하지 않는다. ## Claude ``` personal vault clone URL 알려줘 ``` ## 다음 - [`vault_sync`](/reference/vault-sync) - [clone 가이드](/vault/clone) --- # vault_connect_status ::: tip ✅ 라이브 (서버) `vault_*` / `sync_*` 도구는 `api.ainote.dev` 서버에서 **동작합니다** — JSON-RPC `POST /api/mcp` 또는 [`@ainote/sdk`](/build/sdk) (`ai.vault.*` / `ai.sync.*`)로 호출하세요. (일부 vault 도구는 연결된 git-backed vault가 필요. 참고: `@ainote/mcp` npm 패키지 구버전엔 번들이 안 됐을 수 있으니 직접 JSON-RPC 또는 SDK 사용 권장.) ::: 사용자가 ainote **GitHub App** 을 설치했는지 확인한다. Read-only — GitHub API 를 조회한다. (per-vault 진단이 아니라 계정 단위 설치 상태다.) ## 시그니처 ```json { "name": "vault_connect_status", "arguments": {} } ``` ## 응답 **연결됨:** ```json { "connected": true, "account_login": "me", "account_type": "User", "installation_id": 12345678 } ``` **미연결:** ```json { "connected": false, "install_url": "https://github.com/apps/ainoteapp/installations/new" } ``` 미연결이면 `install_url` 을 사용자에게 안내해 GitHub App 을 설치하게 한다. 설치 후 [`vault_create`](/reference/vault-create) 로 vault(private repo)를 만든다. ## 사용 ``` GitHub 연결됐는지 확인해줘 ``` ## 다음 - [`vault_create`](/reference/vault-create) - [`vault_clone`](/reference/vault-clone) --- # vault_create ::: tip ✅ 라이브 (서버) `vault_*` / `sync_*` 도구는 `api.ainote.dev` 서버에서 **동작합니다** — JSON-RPC `POST /api/mcp` 또는 [`@ainote/sdk`](/build/sdk) (`ai.vault.*` / `ai.sync.*`)로 호출하세요. (일부 vault 도구는 연결된 git-backed vault가 필요. 참고: `@ainote/mcp` npm 패키지 구버전엔 번들이 안 됐을 수 있으니 직접 JSON-RPC 또는 SDK 사용 권장.) ::: 새 vault 를 private GitHub repo 로 생성한다. GitHub App 설치가 선행돼야 한다. 자세한 가이드: [vault/create](/vault/create). ## 시그니처 ```json { "name": "vault_create", "arguments": { "name": "personal", "description": "개인 노트" } } ``` | 파라미터 | 타입 | 필수 | 설명 | |---------|------|------|------| | `name` | string | ✅ | vault 이름 (slug → GitHub repo 이름) | | `description` | string | | GitHub repo 설명 | ## 응답 ```json { "slug": "personal", "github_repo_full_name": "me/personal", "is_primary": false, "indexed_files_count": 0 } ``` repo 는 private 으로 생성된다. 로컬 clone URL 은 [`vault_clone`](/reference/vault-clone) 으로 회수한다. ## 에러 | 코드 | 메시지 | |------|-------| | -32004 | GitHub App 미설치 (vault_connect_status 확인) | | -32603 | repo 이름 충돌 | ## Claude ``` ainote 에 "personal" vault 만들어줘 ``` ## 다음 - [`vault_connect_status`](/reference/vault-connect-status) - [`vault_clone`](/reference/vault-clone) - [Vault 개요](/vault/overview) --- # vault_list ::: tip ✅ 라이브 (서버) `vault_*` / `sync_*` 도구는 `api.ainote.dev` 서버에서 **동작합니다** — JSON-RPC `POST /api/mcp` 또는 [`@ainote/sdk`](/build/sdk) (`ai.vault.*` / `ai.sync.*`)로 호출하세요. (일부 vault 도구는 연결된 git-backed vault가 필요. 참고: `@ainote/mcp` npm 패키지 구버전엔 번들이 안 됐을 수 있으니 직접 JSON-RPC 또는 SDK 사용 권장.) ::: 사용자의 GitHub-backed vault 목록. Read-only. ## 시그니처 ```json { "name": "vault_list", "arguments": {} } ``` ## 응답 ```json [ { "slug": "personal", "github_repo_full_name": "me/personal", "is_primary": true, "indexed_files_count": 123 }, { "slug": "work", "github_repo_full_name": "me/work", "is_primary": false, "indexed_files_count": 56 } ] ``` - `github_repo_full_name` — vault 의 GitHub repo (`owner/repo`) - `is_primary` — 핸드오프·메모리 등 기본 대상 vault 여부 - `indexed_files_count` — ainote 가 인덱싱한 파일 수 clone URL 은 [`vault_clone`](/reference/vault-clone) 으로 회수한다. ## Claude ``` 내 vault 다 보여줘 ``` ## 다음 - [`vault_clone`](/reference/vault-clone) - [Vault 개요](/vault/overview) --- # vault_sync ::: tip ✅ 라이브 (서버) `vault_*`/`sync_*` 도구는 `api.ainote.dev` 서버에서 **동작합니다** — JSON-RPC `POST /api/mcp` 또는 [`@ainote/sdk`](/build/sdk)(`ai.vault.*` / `ai.sync.*`)로 호출하세요. (참고: `@ainote/mcp` npm 패키지의 구버전엔 번들이 안 됐을 수 있으니, 직접 JSON-RPC 또는 SDK 사용을 권장합니다.) ::: vault 양방향 동기화. 자세한 가이드: [vault/sync](/vault/sync). ## 시그니처 ```json { "name": "vault_sync", "arguments": { "name": "personal", "auto_commit": true, "commit_message": "sync from macbook", "strategy": "merge" } } ``` | 파라미터 | 타입 | 기본 | 설명 | |---------|------|------|------| | `name` | string | — | ✅ vault 이름 | | `auto_commit` | boolean | true | 미커밋 변경 자동 commit | | `commit_message` | string | `"sync from "` | | | `strategy` | `merge` / `rebase` / `theirs` / `ours` | `merge` | 충돌 시 | ## 응답 ```json { "vault": "personal", "pulled": 3, "pushed": 1, "files_changed": [ { "path": "daily/2026-05-07.md", "action": "added" }, { "path": "projects/ainote.md", "action": "modified" } ], "conflicts": [] } ``` ## 충돌 시 ```json { "conflicts": [ { "path": "ideas.md", "marker_lines": [12, 24] } ] } ``` 수동 해결 후 재실행: ```bash cd ~/notes/personal $EDITOR ideas.md git add . && git commit ainote vault_sync '{"name":"personal"}' ``` ## 에러 | 코드 | 메시지 | |------|-------| | -32603 | merge conflict | | -32603 | local repo missing | → vault_clone 먼저 | ## Claude ``` personal vault 동기화 모든 vault sync ``` ## 다음 - [`vault_list`](/reference/vault-list) - [Vault sync 자세히](/vault/sync) --- # sync-now 알고리즘 ::: tip ✅ 라이브 (서버) `vault_*` / `sync_*` 도구는 `api.ainote.dev` 서버에서 **동작합니다** — JSON-RPC `POST /api/mcp` 또는 [`@ainote/sdk`](/build/sdk) (`ai.vault.*` / `ai.sync.*`)로 호출하세요. (일부 vault 도구는 연결된 git-backed vault가 필요. 참고: `@ainote/mcp` npm 패키지 구버전엔 번들이 안 됐을 수 있으니 직접 JSON-RPC 또는 SDK 사용 권장.) ::: `~/.claude/ainote-sync/tools/sync-now.sh` 의 의사코드. ## 한 사이클 ```python def sync_now(dry_run=False): manifest = load("manifest.yml") state = load("state.json") log = open("log/{YYYY-MM}.jsonl", "a") # 1. 로컬 스캔 local_files = expand(manifest.sync) # glob 풀어서 (source, target) pair 리스트 # 2. 원격 list remote_files = mcp_call("sync_list", {}) # hub 에서 manifest 매칭 파일들 # 3. 양쪽 sha256 + mtime + hlc 수집 pairs = match_pairs(local_files, remote_files) # 4. 각 페어별 케이스 결정 actions = [] for p in pairs: case = decide_case(p, threshold_minutes=5) actions.append((p, case)) # 5. Safety guard delete_count = sum(1 for _, c in actions if c == "delete") if delete_count / len(actions) > manifest.safety.max_delete_pct / 100: abort("max_delete_pct exceeded") # 6. dry-run 모드 if dry_run: print_actions(actions) return # 7. 실행 for p, case in actions: match case: case "no-op": pass case "push": push_to_hub(p) case "pull": pull_from_hub(p) case "conflict_save_both": save_conflict_files(p) ABORT_PROMPT(f"수동 해결: {p.target}") case "lww_remote_wins": pull_from_hub(p) case "lww_local_wins": push_to_hub(p) case "delete_local": delete_local(p) case "delete_remote": delete_remote(p) log.write(json.dumps({ "ts": now_iso(), "device": DEVICE_ID, "path": p.target, "action": case, "local_sha": p.local_sha, "remote_sha": p.remote_sha, "hlc": p.new_hlc, }) + "\n") update_state(state, p, case) save(state) ``` ## decide_case ```python def decide_case(pair, threshold_minutes): local_changed = (pair.local_sha != state[pair.target].local_sha) remote_changed = (pair.remote_hlc > state[pair.target].hlc) if not local_changed and not remote_changed: return "no-op" if local_changed and not remote_changed: if not pair.local_exists: return "delete_remote" return "push" if not local_changed and remote_changed: if not pair.remote_exists: return "delete_local" return "pull" # 양쪽 변경 time_diff = abs(pair.local_mtime - pair.remote_mtime) if time_diff <= timedelta(minutes=threshold_minutes): return "conflict_save_both" if pair.local_hlc > pair.remote_hlc: return "lww_local_wins" return "lww_remote_wins" ``` ## sub-procedures ### push_to_hub ```python def push_to_hub(p): new_hlc = compute_hlc(now(), state[p.target].hlc, DEVICE_ID) mcp_call("sync_push", { "path": p.target, "content": read_file(p.source), "sha256": p.local_sha, "hlc": new_hlc, "device_id": DEVICE_ID, }) p.new_hlc = new_hlc ``` ### pull_from_hub ```python def pull_from_hub(p): res = mcp_call("sync_pull", {"path": p.target}) write_file(p.source, res.content) set_mtime(p.source, res.remote_mtime) p.new_hlc = res.hlc ``` ### save_conflict_files ```python def save_conflict_files(p): ts = now().strftime("%Y-%m-%dT%H-%M-%S") safe_path = p.target.replace("/", "__") out = f"~/.claude/ainote-sync/conflicts/{ts}__{safe_path}.diff" write(out, format_diff( local=read_file(p.source), local_hlc=p.local_hlc, remote=mcp_call("sync_pull", {"path": p.target}).content, remote_hlc=p.remote_hlc, )) ``` ## 트리거 ### 수동 ```bash ~/.claude/ainote-sync/tools/sync-now.sh ~/.claude/ainote-sync/tools/sync-now.sh --dry-run ``` ### Cron (15분마다) ```bash */15 * * * * /Users/seunghan/.claude/ainote-sync/tools/sync-now.sh >> /tmp/ainote-sync.log 2>&1 ``` ### launchd (macOS, 더 안정적) [vault_sync 와 동일 패턴](/vault/sync) — `.plist` 에 `StartInterval: 900`. ### File watcher (실시간) ```bash fswatch ~/CLAUDE.md ~/.claude/projects/ | xargs -I{} sync-now.sh ``` ## audit (주간) ```bash ~/.claude/ainote-sync/tools/audit.sh ``` 체크: - manifest 의 source 가 모두 존재 - state.json 과 hub 의 file 목록 차이 - conflicts/ 의 미해결 파일 - 최근 7일 변경 통계 (디바이스별) ## 다음 - [충돌 해결](/sync/conflicts) - [HLC 시간 모델](/sync/hlc) - [manifest.yml](/sync/manifest) --- # 아키텍처 — Star Topology ::: tip ✅ 라이브 (서버) `vault_*` / `sync_*` 도구는 `api.ainote.dev` 서버에서 **동작합니다** — JSON-RPC `POST /api/mcp` 또는 [`@ainote/sdk`](/build/sdk) (`ai.vault.*` / `ai.sync.*`)로 호출하세요. (일부 vault 도구는 연결된 git-backed vault가 필요. 참고: `@ainote/mcp` npm 패키지 구버전엔 번들이 안 됐을 수 있으니 직접 JSON-RPC 또는 SDK 사용 권장.) ::: ainote sync 는 **hub-and-spoke** (별 모양). P2P (디바이스끼리 직접) 안 함. ## 그림 ``` ┌────────────────────┐ │ ainote hub │ │ api.ainote.dev │ │ (PostgreSQL+Git) │ └─────────┬──────────┘ │ ┌──────────────┬───────┼───────┬──────────────┐ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ [macmini] [macbook] [iOS] [iPad] [linux desktop] spoke 1 spoke 2 s.3 s.4 s.5 ``` 각 spoke 는 hub 와만 sync. spoke ↔ spoke 직접 X. ## 왜 P2P 가 아닌가 [rclone bisync 포럼 컨센서스](https://forum.rclone.org/t/bisync-many-devices/30043): > "If you are bisyncing more than two devices, set them up in a **star topology** — each spoke synced with the same hub consistently. **Never device-to-device**." 이유: - N 개 디바이스 → P2P 페어 N(N-1)/2 (3개 → 3, 5개 → 10, 10개 → 45) - 모든 페어가 충돌 가능 → 디버깅 불가능 - Star: N 개 → N 페어. 모든 충돌이 hub 에서 한 곳에서 ## hub 의 역할 ``` ┌────────────────────────────────────────────────┐ │ ainote hub │ ├────────────────────────────────────────────────┤ │ 1. 모든 변경 (commit + HLC) 단일 진실 저장 │ │ 2. spoke 의 last_known_state 추적 │ │ 3. 충돌 감지 (5분 임계) │ │ 4. 모든 spoke 에 push 알림 (FCM/APNs/WebSocket) │ │ 5. 부분 sync — manifest 기준 필터 │ │ 6. WORM audit log (변경 이력 영구) │ └────────────────────────────────────────────────┘ ``` ## spoke 의 역할 ``` ┌────────────────────────────────────────────────┐ │ spoke (디바이스) │ ├────────────────────────────────────────────────┤ │ 1. 로컬 manifest.yml — 어떤 파일을 어디로 │ │ 2. state.json — 마지막 sync 상태 │ │ 3. sync-now.sh — 수동 sync 트리거 │ │ 4. conflicts/ — 5분 임계 깨면 양쪽 보존 │ │ 5. log/*.jsonl — 월별 이벤트 로그 (append-only) │ └────────────────────────────────────────────────┘ ``` 자세히: [manifest.yml 작성](/sync/manifest), [sync-now 알고리즘](/sync/algorithm). ## 한 사이클 ``` [spoke A 변경] [hub] [spoke B] │ │ │ │ sync_push │ │ ├───────────────────►│ │ │ │ WebSocket notify │ │ ├─────────────────────►│ │ │ │ │ │ sync_pull │ │ │◄─────────────────────┤ │ │ │ │ │ 변경 전송 │ │ ├─────────────────────►│ ``` 평균 지연: 1초 (WebSocket) ~ 30초 (cron). ## 페일오버 hub 다운 시: - spoke 는 로컬 작업 계속 - 변경은 로컬 log/jsonl 에 누적 - hub 복구 시 일괄 push hub 데이터 손실 시: - 모든 spoke 의 git working tree 가 백업 - spoke 1대를 새 hub 으로 promote 가능 ## 새 디바이스 추가 ``` 1. 새 기기에 manifest.yml + device.id 생성 2. ainote sync_pull --initial 3. hub 의 모든 sync 항목 다운로드 4. state.json 초기화 5. 정상 sync 사이클 시작 ``` 자세히: [새 디바이스 셋업](/examples/new-device). ## 다음 - [HLC 시간 모델](/sync/hlc) - [충돌 해결](/sync/conflicts) - [manifest.yml 작성](/sync/manifest) --- # 충돌 해결 (LWW + 5분 임계) ::: tip ✅ 라이브 (서버) `vault_*` / `sync_*` 도구는 `api.ainote.dev` 서버에서 **동작합니다** — JSON-RPC `POST /api/mcp` 또는 [`@ainote/sdk`](/build/sdk) (`ai.vault.*` / `ai.sync.*`)로 호출하세요. (일부 vault 도구는 연결된 git-backed vault가 필요. 참고: `@ainote/mcp` npm 패키지 구버전엔 번들이 안 됐을 수 있으니 직접 JSON-RPC 또는 SDK 사용 권장.) ::: > 핵심: **5분 안에 양쪽 변경 → 양쪽 보존 + 사용자 선택. 5분 이후 → LWW.** ## 4가지 케이스 | 케이스 | 로컬 변경 | 원격 변경 | 처리 | |--------|----------|----------|------| | **A** | ❌ | ❌ | no-op | | **B** | ✅ | ❌ | push | | **C** | ❌ | ✅ | pull | | **D** | ✅ | ✅ | conflict (아래 참고) | ## 케이스 D 세분화 ``` 시간 차 = |로컬 mtime - 원격 mtime| if 시간 차 ≤ 5분: → 양쪽 보존 (conflict 디렉토리) → 사용자가 수동 머지 else: → LWW (HLC 기준 더 큰 쪽 승) ``` ### D-1: 시간 차 ≤ 5분 → 사용자 선택 양쪽이 거의 동시에 변경 → 의도적 동시 작업 가능 → 자동 결정 위험. 처리: ```bash ~/.claude/ainote-sync/conflicts/ └── 2026-05-07T14-30-00__global__CLAUDE.md.diff ``` `.diff` 파일 형식: ```diff === LOCAL (macmini, 2026-05-07T14:28:00, hlc 2026-05-07T14:28:00.0.macmini) new line from macmini === REMOTE (macbook, 2026-05-07T14:30:00, hlc 2026-05-07T14:30:00.0.macbook) new line from macbook ``` 다음 sync 가 막힘. 사용자가 해결: ```bash ainote sync_resolve global/CLAUDE.md \ --keep local # 또는 --keep remote # 또는 --merge /tmp/manual.md # 수동 머지 결과 파일 ``` 해결 후 다음 sync 진행. ### D-2: 시간 차 > 5분 → LWW 오래된 변경은 stale 가능성 높음 → 자동 결정. ``` HLC 비교: local (HLC 2026-05-07T14:00:00.0.macmini) remote (HLC 2026-05-07T15:30:00.0.macbook) → remote 승 → 로컬 덮어씀 ``` 로컬 버전은 git history 에 남음 → 복구 가능. ## Safety guard `sync-now.sh` 에 안전장치: ```yaml # manifest.yml safety: max_delete_pct: 10 # 10% 이상 파일 삭제 시 abort max_overwrite_files: 5 # 한번에 5파일 이상 덮어쓰기 시 confirm dry_run_default: false ``` `max_delete_pct` 초과 시: ``` ⚠️ ABORT: 53 파일 중 12 파일 삭제 예정 (22%) 설정의 max_delete_pct: 10 초과 실수 의심 — 다음 명령으로 강제: ainote sync_now --override-safety --max-delete-pct=25 ``` ## 충돌 디렉토리 정리 ```bash # 미해결 충돌 목록 ls -lt ~/.claude/ainote-sync/conflicts/ # 7일 지난 해결된 것 자동 정리 (계획됨) ainote sync_cleanup_conflicts --older-than 7d ``` ## Git 으로 해결 (vault) vault 의 경우 일반 git 3-way merge: ```bash cd ~/notes/personal git status # conflict 파일 확인 # editor 로 <<<<<<< 마커 해결 git add . git commit ainote vault_sync personal # 다시 시도 ``` vault 는 git 표준이므로 5분 임계 안 적용. ## 메모리 / dev_doc 의 경우 API 호출 단위라 충돌 감지 다름: - 양쪽이 거의 동시 `update_dev_doc` → HLC 비교 - 5분 임계 안 함 — 항상 LWW (단순) - git history 에 두 버전 모두 보존 ## 다음 - [HLC 시간 모델](/sync/hlc) - [sync-now 알고리즘](/sync/algorithm) - [manifest.yml 작성](/sync/manifest) --- # 폴더 구조 권장 ::: tip ✅ 라이브 (서버) `vault_*` / `sync_*` 도구는 `api.ainote.dev` 서버에서 **동작합니다** — JSON-RPC `POST /api/mcp` 또는 [`@ainote/sdk`](/build/sdk) (`ai.vault.*` / `ai.sync.*`)로 호출하세요. (일부 vault 도구는 연결된 git-backed vault가 필요. 참고: `@ainote/mcp` npm 패키지 구버전엔 번들이 안 됐을 수 있으니 직접 JSON-RPC 또는 SDK 사용 권장.) ::: ainote vault 안의 디렉토리 구조 베스트 프랙티스. ## 표준 구조 ``` ainote vault (hub git repo) ├── global/ # 디바이스 무관 전역 │ ├── CLAUDE.md # ~/CLAUDE.md │ ├── PERSONAS.md # ~/.claude/PERSONAS.md │ ├── ORCHESTRATOR.md │ ├── memory/ # 토픽 메모리 │ │ ├── MEMORY.md │ │ ├── projects-tech-stack.md │ │ └── render-services.md │ └── skills/ # Claude Code 스킬 │ └── seunghan-32inch-ppt/ │ ├── SKILL.md │ └── assets/ │ ├── projects/ # 프로젝트별 (대안: 각 root) │ ├── launchcrew/ │ │ ├── CLAUDE.md │ │ ├── memory/ │ │ │ ├── MEMORY.md │ │ │ └── firebase.md │ │ └── env-ref.md │ ├── tennis-bracket/ │ ├── keeps/ │ └── ... │ ├── dev/ # 도구별 룰 (자동 분류) │ ├── claude/ # category: claude │ ├── cursor/ # category: cursor │ ├── windsurf/ │ ├── copilot/ │ └── docs/ │ ├── env-refs/ # .env 키 목록 (값 X) │ ├── tennis-bracket-env-ref.md │ └── launchcrew-env-ref.md │ └── private/ # 민감 (별도 권한) └── saju/ └── personal-notes.md ``` ## 두 가지 스타일 ### 스타일 A: 카테고리 우선 (`dev/claude/`) 장점: - ainote 자동 분류와 일치 - 같은 종류 한눈에 (모든 CLAUDE.md) - `pull_dev_docs --category claude` 한 방 단점: - 프로젝트별 보기 어려움 - 같은 프로젝트 파일이 여러 디렉토리 흩어짐 ### 스타일 B: 프로젝트 우선 (`projects/launchcrew/`) 장점: - 한 프로젝트 모든 파일 한 폴더 - 새 프로젝트 추가 = 폴더 추가 - Obsidian 에서 보기 좋음 단점: - 자동 분류 안 됨 (수동 manifest) - "모든 CLAUDE.md" 한방에 보기 어려움 ### 권장: 혼합 ``` ainote vault ├── global/ # 전역 → 카테고리 우선 (단순) ├── projects/ # 프로젝트 → 프로젝트 우선 │ └── {project}/ │ ├── CLAUDE.md │ └── memory/ └── dev/ # 자동 분류 카테고리 (Dev Docs) ├── cursor/ # ainote 자동 └── windsurf/ ``` ## 명명 규칙 ### 프로젝트 - **kebab-case** 통일 (`tennis-bracket`, `krx-listing`) - snake → kebab 변환 (`tennis_bracket` → `tennis-bracket`) - 약어 풀어쓰기 (`ai-do` 보다 `ai-do-vibestudy` 처럼 명확하게) ### 파일 | 패턴 | 예시 | |------|------| | `{project}-claude.md` | `launchcrew-claude.md` | | `{project}-memory.md` | `launchcrew-memory.md` | | `{project}-{topic}.md` | `launchcrew-firebase.md` | | `global-{topic}.md` | `global-tech-stack.md` | | `{project}-env-ref.md` | `tennis-bracket-env-ref.md` | ### 메모리 파일 (4 type) - `feedback-{topic}.md` - `project-{topic}.md` - `reference-{topic}.md` - `user-{topic}.md` ## 안 좋은 예 ``` ❌ ainote vault ├── My Notes/ ← 공백 ├── claude_md_files/ ← snake ├── 2026-temp-stuff/ ← 시간 prefix (검색 어려움) ├── important-do-not-delete-final-v2/ ← 의미 없는 prefix └── 한국어 폴더명/ ← 가능하지만 일관성 떨어짐 ``` ✅ 영문 kebab-case, 의미 명확, 깊이 3단 이내. ## 깊이 권장 ``` vault/ ├── A/ ← 1단계 │ ├── B/ ← 2단계 │ │ └── file.md ← 3단계 OK │ │ └── C/ ← 4단계 비추 (검색/관리 어려움) ``` ## 마이그레이션 기존 구조 → 표준 구조: ```bash # 1. 백업 git tag pre-migration-2026-05-07 # 2. 이동 git mv messy-folder global/ # 3. manifest 갱신 $EDITOR ~/.claude/ainote-sync/manifest.yml # 4. 검증 ainote sync_validate # 5. dry-run sync-now.sh --dry-run # 6. 실행 sync-now.sh ``` 자세히: [ainote-sync-redesign 설계 문서](https://github.com/seunghan91/ainote/blob/main/docs/sync-redesign.md). ## 다음 - [manifest.yml 작성](/sync/manifest) - [sync-now 알고리즘](/sync/algorithm) - [메모리 카테고리 / 네이밍](/memory/categories) --- # HLC 시간 모델 ::: tip ✅ 라이브 (서버) `vault_*` / `sync_*` 도구는 `api.ainote.dev` 서버에서 **동작합니다** — JSON-RPC `POST /api/mcp` 또는 [`@ainote/sdk`](/build/sdk) (`ai.vault.*` / `ai.sync.*`)로 호출하세요. (일부 vault 도구는 연결된 git-backed vault가 필요. 참고: `@ainote/mcp` npm 패키지 구버전엔 번들이 안 됐을 수 있으니 직접 JSON-RPC 또는 SDK 사용 권장.) ::: **Hybrid Logical Clock** — wall clock + counter 조합. 시계가 어긋나도 인과 순서 보장. ## 왜 wall clock 만은 안 되나 문제 시나리오: ``` 맥미니 시계: 2026-05-07 14:00 (정상) 맥북 시계: 2026-05-08 14:00 (24시간 빠름, NTP 미동기화) 14:00 (맥미니): CLAUDE.md 에 줄 추가 → push 14:01 (맥북, 실제 14:01 이지만 시계는 +1일): 같은 파일 다른 줄 추가 → push (timestamp = 2026-05-08 14:01) LWW 적용: 맥미니 변경 (timestamp 2026-05-07) < 맥북 변경 (timestamp 2026-05-08) → 맥북 변경이 이김 → 맥미니의 변경 24시간 후 push 해도 무시됨 ``` ## HLC 알고리즘 각 spoke 가 `(timestamp, counter, device_id)` 트리플 유지. ### 로컬 이벤트 (write) ```python def event_local(now_wall_clock, last_hlc): if now_wall_clock > last_hlc.ts: return HLC(ts=now_wall_clock, counter=0, device=DEVICE_ID) else: return HLC(ts=last_hlc.ts, counter=last_hlc.counter+1, device=DEVICE_ID) ``` ### 원격 이벤트 받음 (pull) ```python def event_received(now_wall_clock, last_hlc, remote_hlc): new_ts = max(now_wall_clock, last_hlc.ts, remote_hlc.ts) if new_ts == last_hlc.ts == remote_hlc.ts: new_counter = max(last_hlc.counter, remote_hlc.counter) + 1 elif new_ts == last_hlc.ts: new_counter = last_hlc.counter + 1 elif new_ts == remote_hlc.ts: new_counter = remote_hlc.counter + 1 else: new_counter = 0 return HLC(ts=new_ts, counter=new_counter, device=DEVICE_ID) ``` ## 핵심 속성 1. **Monotonicity**: 같은 spoke 의 다음 HLC 는 항상 이전보다 큼 2. **Causality**: A 가 B 의 원인이면 HLC(A) < HLC(B) 3. **Bounded drift**: 정상 NTP 환경에서 wall clock 과 거의 같음 4. **Tie-breaker**: 같은 timestamp 면 counter, 같은 counter 면 device_id ## 비교 함수 ```python def hlc_compare(a, b): if a.ts != b.ts: return a.ts - b.ts if a.counter != b.counter: return a.counter - b.counter return cmp(a.device, b.device) # 알파벳 순 ``` ## 우리 시나리오 재현 ``` HLC(맥미니, 2026-05-07 14:00, c=0, dev="macmini") ← 변경 push HLC(맥북, ?, ?, dev="macbook"): - 맥북 시계 2026-05-08 14:01 - 마지막 본 hub HLC: (맥미니의 hlc above) - new_ts = max(맥북 wall, 마지막 본) = 2026-05-08 14:01 - HLC(2026-05-08 14:01, c=0, dev="macbook") push 비교: 맥북 (2026-05-08 14:01) > 맥미니 (2026-05-07 14:00) → 맥북 이김 → 같은 결과처럼 보임 ``` 문제 같지만 — **이번엔 맥미니가 추가 변경 시**: ``` 맥미니 다시 작업: - wall = 2026-05-07 14:30 - 마지막 본 hub HLC (맥북) = 2026-05-08 14:01 - new_ts = max(14:30, 2026-05-08 14:01) = 2026-05-08 14:01 - counter += 1 - HLC(2026-05-08 14:01, c=1, dev="macmini") 맥북 (2026-05-08 14:01, c=0) < 맥미니 (2026-05-08 14:01, c=1) → 맥미니 이김 ✅ ``` 즉 — 시계 어긋나도 **연속 변경의 인과 순서는 보존**. ## 우리 시스템에서의 위치 ``` state.json (각 spoke) { "files": { "global/CLAUDE.md": { "hlc": "2026-05-08T14:01:00.000Z.1.macmini", ... } } } ``` `hlc` 필드: `{ISO timestamp}.{counter}.{device_id}`. hub 도 같은 형식 저장. push 시 비교. ## 더 읽기 - Adam Wulf, ["Distributed Clocks and CRDTs"](https://adamwulf.me/2021/05/distributed-clocks-and-crdts/) (2021) - Jared Forsyth, ["Hybrid Logical Clocks"](https://jaredforsyth.com/posts/hybrid-logical-clocks/) - 원 논문: Kulkarni et al., ["Logical Physical Clocks"](https://cse.buffalo.edu/tech-reports/2014-04.pdf) (2014) ## 다음 - [충돌 해결 (5분 임계)](/sync/conflicts) - [sync-now 알고리즘](/sync/algorithm) --- # manifest.yml 작성 ::: tip ✅ 라이브 (서버) `vault_*` / `sync_*` 도구는 `api.ainote.dev` 서버에서 **동작합니다** — JSON-RPC `POST /api/mcp` 또는 [`@ainote/sdk`](/build/sdk) (`ai.vault.*` / `ai.sync.*`)로 호출하세요. (일부 vault 도구는 연결된 git-backed vault가 필요. 참고: `@ainote/mcp` npm 패키지 구버전엔 번들이 안 됐을 수 있으니 직접 JSON-RPC 또는 SDK 사용 권장.) ::: 각 디바이스의 sync 매핑 정책. 로컬 파일 ↔ ainote path 명시적 정의. ## 위치 ``` ~/.claude/ainote-sync/manifest.yml ``` 같은 디렉토리에 `device.id` (UUID) + `state.json` (last-known sync 상태). ## 최소 예시 ```yaml device_id: macmini-2026-04-A1B2 hub: api: https://api.ainote.dev/api/mcp auth_env: AINOTE_API_KEY sync: - source: ~/CLAUDE.md target: global/CLAUDE.md ``` ## 전체 예시 ```yaml device_id: macmini-2026-04-A1B2 hub: api: https://api.ainote.dev/api/mcp auth_env: AINOTE_API_KEY sync: # 글로벌 메모리 — glob (모든 .md) - source: ~/.claude/projects/-Users-seunghan/memory/ pattern: "*.md" target: global/memory/{basename} type: glob # 1:1 명시 - source: ~/CLAUDE.md target: global/CLAUDE.md - source: ~/.claude/PERSONAS.md target: global/PERSONAS.md # 디렉토리 통째 (재귀) - source: ~/.claude/skills/seunghan-32inch-ppt/ pattern: "**/*" target: global/skills/seunghan-32inch-ppt/{relpath} type: glob_recursive # 프로젝트별 메모리 (모든 프로젝트) - source: ~/.claude/projects/{project}/memory/ pattern: "*.md" target: "{project_name}/{basename}" type: glob_per_project # 프로젝트 root CLAUDE.md (화이트리스트) - source: ~/launchcrew/CLAUDE.md target: launchcrew/CLAUDE.md - source: ~/tennis_bracket/CLAUDE.md target: tennis-bracket/CLAUDE.md # snake → kebab # 명시적 제외 skip: - ~/.claude/COMMANDS.md - ~/.claude/FLAGS.md - ~/.claude/RULES.md - ~/.claude/MCP.md # 원격에 있지만 더 이상 받지 않을 (deprecated) deprecated_remote: - krx_listing_backups/ - krx_listing/ # snake (kebab 으로 마이그) safety: max_delete_pct: 10 max_overwrite_files: 5 dry_run_default: false ``` ## sync 항목 형식 ### 단순 1:1 ```yaml - source: ~/file.md target: ainote/path.md ``` ### Glob (디렉토리 + 패턴) ```yaml - source: ~/dir/ pattern: "*.md" target: dest/{basename} type: glob ``` placeholder: - `{basename}` — 파일명만 (`MEMORY.md`) - `{filename}` — 확장자 제외 (`MEMORY`) - `{relpath}` — source 기준 상대경로 ### 재귀 Glob ```yaml - source: ~/.claude/skills/my-skill/ pattern: "**/*" target: skills/my-skill/{relpath} type: glob_recursive ``` ### 프로젝트별 (특수) ```yaml - source: ~/.claude/projects/{project}/memory/ pattern: "*.md" target: "{project_name}/{basename}" type: glob_per_project ``` `{project}` 는 디렉토리 이름, `{project_name}` 은 변환 (snake→kebab, prefix 제거 등). ### 변환 규칙 (선택) ```yaml - source: ~/{project}/CLAUDE.md target: "{project_kebab}/CLAUDE.md" transform: project_kebab: from: project replace: "_": "-" ``` ## 검증 manifest 변경 후: ```bash ainote sync_validate ``` 체크: - YAML 문법 - source 경로 존재 - target 충돌 (같은 target 에 두 source) - circular reference ## 다른 디바이스의 manifest 각 디바이스가 **독립적** manifest. 한 hub 에 여러 manifest 가능 → spoke 마다 다른 sync 범위. 예: - 맥미니: 모든 프로젝트 sync - iPad: 메모리만 (project CLAUDE.md 제외) - linux desktop: vault 만 ## 다음 - [sync-now 알고리즘](/sync/algorithm) - [폴더 구조 권장](/sync/folder-structure) - [충돌 해결](/sync/conflicts) --- # 다중 디바이스 동기화 개요 ::: tip ✅ 라이브 (서버) `vault_*` / `sync_*` 도구는 `api.ainote.dev` 서버에서 **동작합니다** — JSON-RPC `POST /api/mcp` 또는 [`@ainote/sdk`](/build/sdk) (`ai.vault.*` / `ai.sync.*`)로 호출하세요. (일부 vault 도구는 연결된 git-backed vault가 필요. 참고: `@ainote/mcp` npm 패키지 구버전엔 번들이 안 됐을 수 있으니 직접 JSON-RPC 또는 SDK 사용 권장.) ::: ainote 는 **Star topology + HLC + LWW** 모델로 여러 디바이스에서 동일한 vault 를 유지합니다. ## 핵심 원칙 1. **Star topology** — 각 디바이스는 ainote vault (hub) 와만 sync. 디바이스끼리 직접 X. 2. **HLC (Hybrid Logical Clock)** — wall-clock + counter 로 시계 어긋남(클럭 드리프트) 방지하면서 시간 기반 정렬. 3. **LWW (Last-Write-Wins)** — 양쪽 변경 시 시점 우선. 단 5분 이내 충돌은 수동 검토. 4. **Local source of truth** — 충돌 시 기본은 로컬 우선 (사용자 의도가 가장 최근). ## 4가지 sync 케이스 ``` sync(file): L = local sha256, R = remote git_sha, S = state.json[file] 1. L == S.local AND R == S.remote → SKIP (변경 없음) 2. L != S.local AND R == S.remote → PUSH (로컬만 변경) 3. L == S.local AND R != S.remote → PULL (원격만 변경) 4. L != S.local AND R != S.remote → CONFLICT 4a. |L_mtime - R_updated| ≥ 5분 → auto LWW (newer wins) 4b. < 5분 → manual (diff 저장 → 사용자 결정) ``` ## 안전장치 - `--dry-run` — 무엇을 push/pull 할지만 출력 (실제 변경 X) - `--max-delete 30%` — 단일 sync 에서 30% 이상 파일 삭제 시 abort - `log/{YYYY-MM}.jsonl` — append-only 이벤트 로그 (디바이스/시점/경로/액션 5-way 추적) - `conflicts/` — 미해결 충돌은 diff 로 보관 (잃어버리지 않음) ## 다음 단계 - [아키텍처 — Star Topology](/sync/architecture) - [HLC 시간 모델](/sync/hlc) - [충돌 해결 (LWW)](/sync/conflicts) - [manifest.yml 작성](/sync/manifest) - [sync-now 알고리즘](/sync/algorithm) ::: tip 설계 문서 이 동기화 시스템의 전체 설계는 [`global/ainote-sync-redesign.md`](https://ainote.dev/sync/redesign) 참조. 산업 패턴 조사 (rclone bisync · Syncthing · Unison · CRDT) + 우리 케이스 적합성 분석 포함. ::: --- # 왜 별도 sync 시스템 (vs Dropbox) ::: tip ✅ 라이브 (서버) `vault_*` / `sync_*` 도구는 `api.ainote.dev` 서버에서 **동작합니다** — JSON-RPC `POST /api/mcp` 또는 [`@ainote/sdk`](/build/sdk) (`ai.vault.*` / `ai.sync.*`)로 호출하세요. (일부 vault 도구는 연결된 git-backed vault가 필요. 참고: `@ainote/mcp` npm 패키지 구버전엔 번들이 안 됐을 수 있으니 직접 JSON-RPC 또는 SDK 사용 권장.) ::: > 짧은 답: **Dropbox 는 파일을 sync 하지만, ainote 는 의미를 sync 한다.** ## 비교 | | Dropbox/iCloud | Syncthing | git | ainote sync | |---|---|---|---|---| | 충돌 감지 | 시간 기반 (silent overwrite) | 양쪽 보존 (`*.sync-conflict-...`) | 3-way merge | HLC + LWW + 5분 임계 | | 의미 알림 | ❌ | ❌ | commit msg | ✅ 디바이스/시점 추적 | | 부분 sync | △ | △ | ✅ | ✅ manifest 기반 | | AI 접근 | ❌ | ❌ | △ | ✅ MCP 도구 | | 다중 디바이스 | ✅ | ✅ | ✅ | ✅ (star topology) | ## 구체적 문제 사례 ### Dropbox 의 silent overwrite 타임라인: ``` 14:00 — 맥미니: CLAUDE.md 에 새 규칙 5줄 추가 → 저장 14:05 — 맥북 (오프라인): 같은 파일 다른 곳 수정 → 저장 14:10 — 맥북 온라인 → Dropbox 동기화 → 맥미니 변경 사라짐 ``` 복구: Dropbox 버전 히스토리 (30일 보관) → 수동 비교 → 머지. ### Git 의 머지 conflict 마커 ``` <<<<<<< HEAD new content from local ======= new content from remote >>>>>>> origin/main ``` 마크다운 한가운데 들어가면 보기 싫음 + Obsidian 에서 깨짐. ### ainote 의 접근 1. **HLC** — 시계 어긋나도 인과 순서 보장 2. **5분 임계** — 그 안에 양쪽 변경 시 conflict 디렉토리에 양쪽 보관, 사용자 선택 3. **5분 후 변경** — LWW (last-writer-wins) — drift 방지 4. **device 추적** — 어느 기기에서 언제 변경했는지 모두 로그 5. **부분 sync** — manifest.yml 로 명시적 매핑만 ## "그래도 Dropbox 가 충분하지 않나?" Dropbox 가 충분한 경우: - 혼자 한 기기에서만 작업 - 또는 절대 같은 파일 동시 편집 X - AI 접근 필요 X ainote sync 가 필요한 경우: - 17개 프로젝트 CLAUDE.md 멀티 기기 - iPhone Telegram 으로 메모 → 맥북 Claude 가 즉시 활용 - AI 가 모든 기기에서 같은 메모리 봐야 함 - "이 변경 어느 기기에서 한 거지" 추적 필요 ## "그러면 그냥 git 만 쓰면 안되나?" git 충분한 경우: - 개발자 본인 익숙 - conflict marker 직접 해결 OK ainote sync 가 필요한 경우: - 비-기술 사용자도 함께 (가족 공유 vault) - AI 가 자동 sync (사용자 git 명령 모름) - 5분 임계처럼 사람-친화 정책 ## 다음 - [동기화 개요](/sync/overview) - [아키텍처 — Star Topology](/sync/architecture) - [HLC 시간 모델](/sync/hlc) --- # 카테고리 태스크 분류 라벨. ## 모델 ```ts Category { id: string (UUID) name: string // "업무", "개인", "운동" color?: string // hex, 예 "#0088ff" icon?: string // emoji 또는 SF Symbol position?: number } ``` ## MCP 도구 ```bash ainote list_categories '{}' ``` 응답 (text + structured resource): ```json { "content": [ { "type": "text", "text": "[Formatted list]" }, { "type": "resource", "resource": { "uri": "ainote://categories/list", "mimeType": "application/json", "text": "{\"categories\":[{\"id\":\"uuid\",\"name\":\"업무\",\"color\":\"#0088ff\"}]}" } } ] } ``` `category_id` 는 **UUID** (정수 아님). `create_task` / `update_task` / `list_tasks` 의 `category_id` 에 넣으면 됨. ## 자연어 매칭 `create_task` 가 카테고리 자동 추론은 안 함 (`category_id` 명시 필요). Claude 가 자연어 발화 → `list_categories` 로 ID 조회 → `create_task` 호출 패턴. ``` 사용자: "이거 운동 카테고리에 추가해줘 — 헬스장 7시" Claude: 1. list_categories → "운동" 의 id 찾음 2. create_task { content: "헬스장", due_date: "...19:00", category_id: "" } ``` ## 카테고리 추가 / 수정 MCP 도구 없음 (현재). 웹 UI 만: - - 색상/아이콘 picker, 정렬 CRUD MCP 도구는 계획됨 (v0.5+). ## 필터링 ```bash ainote list_tasks '{"category_id":""}' ``` ## 다음 - [`list_categories` API](/reference/list-categories) - [필터링 18개](/tasks/filtering) - [태스크 개요](/tasks/overview) --- # 날짜 / 위치 / 알림 ## 날짜 (`due_date` + `due_time`) ### 자연어 인식 `create_task` 의 `content` 안에 시간 표현 → 자동 파싱: | 입력 | 파싱 결과 (한국 기준 2026-05-07 목요일) | |------|---------| | "내일 오전 10시" | `due_date: "2026-05-08T10:00:00+09:00"` | | "다음주 화요일" | `due_date: "2026-05-12"` (시간 없음 → `is_all_day` 가능) | | "오늘 저녁 7시" | `due_date: "2026-05-07T19:00:00+09:00"` | | "5월 20일 3시" | `due_date: "2026-05-20T15:00:00+09:00"` | | "30분 후" | `due_date` = now + 30 min | 명시적 ISO 8601 도 가능: ```json { "content": "회의", "due_date": "2026-05-08T10:00:00+09:00" } ``` ### `due_date` + `due_time` 분리 `due_date` 가 시간 없을 때 `due_time` 만 따로: ```json { "content": "회의", "due_date": "2026-05-08", "due_time": "10:00" } ``` ### 종일 일정 ```json { "content": "휴가", "due_date": "2026-05-08", "is_all_day": true } ``` ### 다일 이벤트 ```json { "content": "출장", "start_date": "2026-05-08", "due_date": "2026-05-10", "is_all_day": true } ``` ## 위치 (`location` + GPS) ### 사람용 텍스트 ```json { "content": "강남역 미팅", "location": "강남역 3번 출구 스타벅스" } ``` 표시할 때 그대로 노출. `list_tasks` 의 `location` 필터에서 부분 일치 검색. ### GPS 좌표 ```json { "content": "스벅", "location": "Starbucks Gangnam", "location_lat": 37.4979, "location_lng": 127.0276 } ``` iOS/Android 앱에서 지도 표시 가능. ## 이동시간 (`travel_time`) 마감 시각 N분 전 이동 시간 확보 (알림 스케줄링 영향): ```json { "content": "강남역 미팅", "due_date": "2026-05-08T10:00:00+09:00", "travel_time": 30, "notification_minutes_before": 60 } ``` → 마감 60분 전 알림 + 30분 전엔 "이동 시간" 표시. ## 알림 (`notification_minutes_before`) ### 기본 ```json { "content": "회의", "due_date": "2026-05-08T10:00:00+09:00", "notification_minutes_before": 30 } ``` → 09:30 에 푸시 (FCM/APNs/Telegram/Web Push 모두). ### 알림 제거 (update 시) ```json { "id": "uuid", "notification_minutes_before": null } ``` `null` 명시적 전달 → 기존 MCP reminder 제거. ### 다중 채널 / 다중 시점 현재 MCP 도구는 단일 알림만 (`notification_minutes_before`). 다중 알림은 웹 UI 또는 `update_task` 후 다시 push. ## 반복 (`repeat_rule`) ### 단순 패턴 ```json { "content": "주간 회고", "due_date": "2026-05-10T14:00:00+09:00", "repeat_rule": "weekly" } ``` 값: `daily` / `weekly` / `monthly` / `yearly`. ### RRULE (iCalendar) ```json { "repeat_rule": "FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20261231T235959Z" } ``` 서버가 부분 지원. 자세한 spec 은 [RFC 5545](https://www.rfc-editor.org/rfc/rfc5545#section-3.3.10). ### 인스턴스 vs 마스터 반복 설정 task 는 "마스터". 표시되는 건 `recurring_instances`: - 마스터 수정 → 모든 미래 인스턴스 영향 - 단일 인스턴스 수정 → 그것만 (예외) ## 우선순위 (`is_important`) `true` 면 별표 + 모든 list 에서 우선 정렬. ```json { "content": "긴급 패치", "is_important": true } ``` ## 다음 - [필터링 18개 전체](/tasks/filtering) - [`create_task` API](/reference/create-task) - [태스크 개요](/tasks/overview) --- # 필터링 (18개 옵션) `list_tasks` 의 모든 필터. ## 상태 | 파라미터 | 값 | 설명 | |---------|-----|------| | `status` | `pending` / `completed` | 기본 없음 (전체) | | `is_important` | boolean | 중요 표시만 | | `overdue` | boolean | 마감 지난 미완료 (`due_date < today`) | | `due_today` | boolean | 오늘 마감 | | `has_notification` | boolean | 알림 설정된 것만 | ## 검색 / 위치 | 파라미터 | 설명 | |---------|------| | `search` | 내용 키워드 (부분 일치) | | `location` | 위치 텍스트 부분 일치 (예: `"여의도"`, `"서울"`) | | `category_id` | 카테고리 UUID | ## 날짜 범위 | 파라미터 | 설명 | |---------|------| | `due_date_start` | ISO 8601 — `due_date >= 이 값` | | `due_date_end` | `due_date <= 이 값` | | `completed_date_start` | 완료일 ≥ | | `completed_date_end` | 완료일 ≤ | | `created_date_start` | 생성일 ≥ | | `created_date_end` | 생성일 ≤ | ## 정렬 / 페이징 | 파라미터 | 값 | 기본 | |---------|-----|------| | `sort_by` | `due_date` / `created_at` / `completed_at` / `updated_at` / `is_important` | `created_at` | | `sort_order` | `asc` / `desc` | `desc` | | `limit` | 1~500 | 25 | ## 자연어 → 시간 계산 ainote MCP 가 자연어 시간 표현을 자동 변환 (today = 2026-05-07 가정): | 입력 | 변환 | |------|------| | "오늘" | `due_today: true` | | "이번 주" | `due_date_start: "2026-05-05", due_date_end: "2026-05-11"` | | "다음 주" | `due_date_start: "2026-05-12", due_date_end: "2026-05-18"` | | "이번 달" | `due_date_start: "2026-05-01", due_date_end: "2026-05-31"` | | "지난 주" | `completed_date_start: "...", completed_date_end: "..."` | | "지난 달" | `completed_date_start: "2026-04-01", completed_date_end: "2026-04-30"` | ## 자주 쓰는 조합 ```jsonc // 오늘 할 일 { "due_today": true, "status": "pending" } // 이번 주 중요한 거 { "is_important": true, "due_date_start": "2026-05-05", "due_date_end": "2026-05-11" } // 마감 지난 미완료 (오래된 순) { "overdue": true, "sort_by": "due_date", "sort_order": "asc" } // 여의도 관련 { "location": "여의도" } // 어제 완료 (회고) { "status": "completed", "completed_date_start": "2026-05-06", "completed_date_end": "2026-05-06" } // "회의" 키워드 + 다음 7일 { "search": "회의", "due_date_start": "2026-05-07", "due_date_end": "2026-05-14" } ``` ## 응답 ```json { "content": [ { "type": "text", "text": "[Formatted list]" }, { "type": "resource", "resource": { "uri": "ainote://tasks/list", "mimeType": "application/json", "text": "{\"tasks\":[...]}" } } ] } ``` 구조화된 JSON 은 `content[1].resource.text` 에. ## 다음 - [`list_tasks` API](/reference/list-tasks) - [태스크 개요](/tasks/overview) - [날짜 / 위치 / 알림](/tasks/due-date) --- # 태스크 개요 ainote 의 첫 번째 1급 시민 — **AI 가 직접 추가/수정/삭제하는 할 일**. ## 핵심 모델 (실제 스키마) ```ts Task { id: string (UUID) content: string // 필수 notes?: string // 자유 메모 / 상세 due_date?: string (ISO 8601) due_time?: string ("HH:MM") start_date?: string (ISO 8601) // 다일 이벤트 is_all_day?: boolean is_important?: boolean category_id?: string (UUID) location?: string location_lat?: number location_lng?: number travel_time?: number // 분 repeat_rule?: string // "daily" / "weekly" / "monthly" / RRULE notification_minutes_before?: number completed_at?: string (ISO) | null } ``` ## 자연어 → 구조화 Claude 에서 "내일 오전 10시 강남역에서 미팅, 30분 전 알려줘" → ```json { "content": "강남역 미팅", "due_date": "2026-05-08T10:00:00+09:00", "location": "강남역", "notification_minutes_before": 30 } ``` ainote MCP 가 시간 표현 ("내일", "다음주 화요일", "오후 3시") + 위치 + 알림 모두 파싱. ## MCP 도구 5개 | 도구 | 용도 | |------|------| | [`create_task`](/reference/create-task) | 새 태스크 (필수: `content`) | | [`update_task`](/reference/update-task) | 수정 (`completed_at` 으로 완료 처리) | | [`delete_task`](/reference/delete-task) | Soft delete (30일 후 영구) | | [`list_tasks`](/reference/list-tasks) | 18개 필터 + 자연어 | | [`list_categories`](/reference/list-categories) | 카테고리 목록 | ## 주요 필터 ([18개 전체](/tasks/filtering)) ``` 오늘 할 일 → status: pending, due_today: true 중요 표시만 → is_important: true "회의" 검색 → search: "회의" 강남 관련 → location: "강남" 완료 (지난주) → status: completed, completed_date_start: "...", completed_date_end: "..." 지난 마감 → overdue: true 알림 설정된 → has_notification: true ``` ## 자동 백그라운드 작업 (Solid Queue) | Job | 주기 | 설명 | |-----|------|------| | `TaskCleanupJob` | 매일 2시 | 30일 지난 soft-delete 영구 삭제 | | `TaskReminderJob` | 5분마다 | 마감 임박 알림 발송 | | `NotificationSchedulerJob` | 15분마다 | 스케줄된 알림 처리 | | `SmartNotificationSchedulerJob` | 매시간 | 일일 요약, 아침 브리핑 | | `NotificationCleanupJob` | 매일 4시 | 3일 지난 읽은 알림 삭제 | | `CleanupStaleFcmTokensJob` | 매일 3시 | 만료 FCM 토큰 정리 | ## 알림 채널 태스크 마감 임박 시 동시 발송: - 📱 iOS / Android 푸시 (FCM + APNs) - 💬 Telegram (계정 연동 시) - 🌐 Web Push (브라우저 PWA) - ⌚ Apple Watch (iOS 동기화) ## 다음 - [카테고리 관리](/tasks/categories) - [필터 18개 전체](/tasks/filtering) - [날짜 / 위치 / 알림 자세히](/tasks/due-date) - [`create_task` API](/reference/create-task) --- # vault_clone ::: tip ✅ 라이브 (서버) `vault_*` / `sync_*` 도구는 `api.ainote.dev` 서버에서 **동작합니다** — JSON-RPC `POST /api/mcp` 또는 [`@ainote/sdk`](/build/sdk) (`ai.vault.*` / `ai.sync.*`)로 호출하세요. (일부 vault 도구는 연결된 git-backed vault가 필요. 참고: `@ainote/mcp` npm 패키지 구버전엔 번들이 안 됐을 수 있으니 직접 JSON-RPC 또는 SDK 사용 권장.) ::: 기존 vault 의 **GitHub clone URL 을 회수**한다. ainote 는 git 트래픽을 proxy 하지 않으므로, 서버가 clone 을 대신 수행하지는 않고 URL 과 실행할 git 명령만 돌려준다. 사용자가 그 명령으로 직접 clone 한다. ## 시그니처 | 인자 | 필수 | 설명 | |---|---|---| | `name` | ✅ | vault 이름 또는 slug | | `target_path` | | 반환 안내에 쓸 로컬 clone 경로 (선호값) | ```json { "name": "personal", "target_path": "~/vaults/personal" } ``` ## 응답 vault 의 GitHub clone URL 과 실행할 git 명령을 돌려준다: ```json { "github_repo_full_name": "me/personal", "clone_url_https": "https://github.com/me/personal.git", "clone_url_ssh": "git@github.com:me/personal.git", "target_path": "~/vaults/personal", "command": "git clone https://github.com/me/personal.git ~/vaults/personal" } ``` ## 새 기기 셋업 반환된 `command` 로 직접 clone 한다 (GitHub 자격증명 사용): ```bash git clone https://github.com/me/personal.git ~/vaults/personal cd ~/vaults/personal ``` 이후 편집·commit·push 는 GitHub 표준 그대로. push 하면 GitHub webhook 이 ainote 를 깨워 재인덱싱한다. ## 전제 — GitHub 연결 vault 가 GitHub 에 연결돼 있어야 clone URL 이 있다. [`vault_connect_status`](/reference/vault-connect-status) 로 설치를 확인하고, 미연결이면 GitHub App 설치 후 [`vault_create`](/vault/create) 로 repo 를 만든다. ## 인증 **GitHub 표준 자격증명** — ainote MCP 키는 clone 에 관여하지 않는다. - `gh auth login` (GitHub CLI) - SSH key — - Personal Access Token (PAT) — HTTPS clone 시 비밀번호 대신 ## 로컬 경로 권장 - 짧고 명시적 (`~/vaults/personal` 좋음, 깊게 중첩된 경로 나쁨) - iCloud Drive **비추** — git 과 충돌 - TimeMachine 백업 대상 폴더 안 **비추** — 백업이 무거워짐 ## Obsidian 호환 vault 는 Obsidian 호환 마크다운 폴더다. clone 후 Obsidian 에서 "Open folder as vault" → 로컬 clone 경로 선택. 임시 캐시는 `.gitignore` 로 제외: ``` # .gitignore (vault root) .obsidian/workspace* .obsidian/cache .obsidian/plugins/*/data.json ``` ## 다음 - [`vault_sync` — vault 파일 read/write](/vault/sync) - [`vault_create` — 새 vault](/vault/create) - [Vault 개요](/vault/overview) --- # vault_create ::: tip ✅ 라이브 (서버) `vault_*` / `sync_*` 도구는 `api.ainote.dev` 서버에서 **동작합니다** — JSON-RPC `POST /api/mcp` 또는 [`@ainote/sdk`](/build/sdk) (`ai.vault.*` / `ai.sync.*`)로 호출하세요. (일부 vault 도구는 연결된 git-backed vault가 필요. 참고: `@ainote/mcp` npm 패키지 구버전엔 번들이 안 됐을 수 있으니 직접 JSON-RPC 또는 SDK 사용 권장.) ::: 새 vault 를 사용자 계정 아래 **private GitHub repository** 로 생성한다. ::: warning 전제 — GitHub App 설치 먼저 ainote GitHub App 을 설치해야 한다. [`vault_connect_status`](/reference/vault-connect-status) 로 확인하고, 미설치면 반환된 `install_url` 로 설치한다. ::: ## 시그니처 ```json { "name": "personal", "description": "개인 노트" } ``` | 파라미터 | 타입 | 필수 | 설명 | |---------|------|------|------| | `name` | string | ✅ | vault 이름. slug 이 파생되어 GitHub repo 이름이 됨 | | `description` | string | | GitHub repo 설명 (선택) | ## 응답 ```json { "slug": "personal", "github_repo_full_name": "me/personal", "is_primary": false, "indexed_files_count": 0 } ``` repo 는 **private** 으로 생성된다 (GitHub repo 가시성 정책을 따름). ## Claude 사용 ``` ainote 에 "personal" vault 만들어줘 ``` ## 생성 직후 ainote 가: 1. 사용자 GitHub 계정에 private repo (`/`) 생성 2. FileIndex 로 인덱싱 시작 (파일이 push 되면 GitHub webhook 으로 재인덱싱) 이후 [`vault_clone`](/vault/clone) 이 준 URL 로 로컬에 clone 한다: ```bash git clone https://github.com/me/personal.git ~/vaults/personal ``` ## 이름 규칙 - 영소문자, 숫자, `-` - slug 은 GitHub repo 이름 규칙을 따름 ## 다음 - [`vault_clone` — clone URL 회수](/vault/clone) - [`vault_sync` — vault 파일 read/write](/vault/sync) - [Vault 개요](/vault/overview) --- # Git backend ::: tip ✅ 라이브 (서버) `vault_*` / `sync_*` 도구는 `api.ainote.dev` 서버에서 **동작합니다** — JSON-RPC `POST /api/mcp` 또는 [`@ainote/sdk`](/build/sdk) (`ai.vault.*` / `ai.sync.*`)로 호출하세요. (일부 vault 도구는 연결된 git-backed vault가 필요. 참고: `@ainote/mcp` npm 패키지 구버전엔 번들이 안 됐을 수 있으니 직접 JSON-RPC 또는 SDK 사용 권장.) ::: ainote 의 vault 는 **너의 GitHub private repository** 다. ainote 는 git 트래픽을 proxy 하지 않는다 — 저장·버전·clone/push 는 GitHub 이 담당하고, ainote 는 파일 인덱싱·검색·MCP 통합만 한다. 철학: **파일이 주인, DB 는 인덱스, git 이 접착제.** ## 구조 ``` GitHub (github.com) └── / └── ← 사용자 소유 private repo ├── *.md ← 노트/핸드오프/메모리 └── ... ``` vault 1개 = GitHub private repo 1개. 사용자가 GitHub 에서 직접 clone 하고, push 하면 GitHub webhook 이 ainote 를 깨워 재인덱싱한다. ainote 서버에 bare repo 를 두거나 git 요청을 중계하지 않는다. ## 연결 (GitHub App) git-backed vault 를 쓰려면 먼저 ainote GitHub App 을 설치한다. 1. `vault_connect_status` — 설치 여부 확인. 미설치면 `install_url` 반환. 2. install_url 로 App 설치 (사용자 계정 또는 org 선택). 3. `vault_create { name }` — 사용자 계정 아래 **private repo** 를 새로 만들어 vault 로 연결. slug 는 name 에서 파생되어 repo 이름이 된다. ```json // vault_connect_status → 미연결 예시 { "connected": false, "install_url": "https://github.com/apps/ainoteapp/installations/new" } ``` ## Clone `vault_clone { name }` 은 vault 의 **GitHub HTTPS clone URL** 을 돌려준다. 인증은 ainote 가 아니라 **네 GitHub 자격증명**으로 한다. ```json // vault_clone { "name": "personal" } → { "clone_url_https": "https://github.com//.git", "clone_url_ssh": "git@github.com:/.git", "command": "git clone https://github.com//.git ~/vaults/personal" } ``` ```bash # 반환된 URL 로 직접 clone (GitHub 인증) git clone https://github.com//.git ~/vaults/personal ``` ## 인증 **GitHub 표준 인증을 그대로 쓴다.** ainote MCP 키는 clone/push 에 관여하지 않는다. - `gh auth login` (GitHub CLI) - SSH key — 에 등록 - Personal Access Token (PAT) — HTTPS clone 시 비밀번호 대신 ## 표준 git 명령 clone 후엔 그냥 GitHub repo 다: ```bash cd ~/vaults/personal git log --oneline git diff git checkout -b experimental git push origin experimental git tag -a v1 -m "snapshot" && git push --tags ``` 특정 시점 복원: ```bash git checkout ``` ainote MCP 도구(`vault_sync` 등)는 기본 `main` 브랜치를 읽고 쓴다. 다른 브랜치는 git CLI 로 직접 다룬다. ## 다중 디바이스 / 백업 vault 가 이미 GitHub 에 있으므로 GitHub 자체가 원격·백업이다. 새 기기는 `vault_clone` 이 준 URL 로 clone 하면 된다. 추가로 다른 호스트에 미러하려면 GitHub 를 소스로 두고 미러 push: ```bash cd ~/vaults/personal git remote add mirror git@gitlab.com:me/notes-mirror.git git push mirror main # cron 으로 정기 push ``` push 는 GitHub 으로, ainote 재인덱싱은 GitHub webhook 으로 자동 처리된다. ## 크기 / 한도 vault 는 GitHub repo 이므로 **GitHub 의 저장소 정책**(repo/파일 크기, LFS 등)을 그대로 따른다. ainote 는 인덱스만 유지하므로 별도 vault 용량 한도를 두지 않는다. 대용량 바이너리는 GitHub 권장대로 Git LFS 사용. ## 관련 도구 - `vault_connect_status` — GitHub App 설치 여부 확인 - `vault_create` — 사용자 계정 아래 private repo vault 생성 - `vault_clone` — vault 의 GitHub clone URL 회수 - `vault_sync` — vault 파일 read/write (기본 `main`) --- # Vault 개요 ::: tip ✅ 라이브 (서버) `vault_*` / `sync_*` 도구는 `api.ainote.dev` 서버에서 **동작합니다** — JSON-RPC `POST /api/mcp` 또는 [`@ainote/sdk`](/build/sdk) (`ai.vault.*` / `ai.sync.*`)로 호출하세요. (일부 vault 도구는 연결된 git-backed vault가 필요. 참고: `@ainote/mcp` npm 패키지 구버전엔 번들이 안 됐을 수 있으니 직접 JSON-RPC 또는 SDK 사용 권장.) ::: ainote 의 **세 번째 1급 시민** — Obsidian 호환 마크다운 저장소, git 백엔드. ## Vault 가 뭔가 **Obsidian** 의 vault 와 동일 개념: - 폴더 1개 = vault 1개 - 안에 마크다운 파일들 자유롭게 (서브폴더 OK) - `[[wikilinks]]` 호환 - 프론트매터 YAML 지원 차이점: - **git 백엔드** — 모든 변경 자동 commit - **MCP 노출** — Claude 가 직접 read/write - **다중 디바이스** — 여러 기기에서 같은 vault clone ## 5개 MCP 도구 | 도구 | 용도 | |------|------| | [`vault_create`](/reference/vault-create) | 새 vault (빈 git repo) | | [`vault_clone`](/reference/vault-clone) | 다른 기기/Obsidian vault 가져오기 | | [`vault_sync`](/reference/vault-sync) | pull → 변경사항 → push 양방향 | | [`vault_list`](/reference/vault-list) | 등록된 vault 목록 | | [`vault_connect_status`](/reference/vault-connect-status) | git 상태 확인 | ## 사용 시나리오 ### A. 처음 시작 — 빈 vault ``` ainote 에 "personal" 이라는 vault 만들어줘 ``` → `vault_create` → git repo 생성 → 다른 도구에서 사용 가능. ### B. 기존 Obsidian 마이그레이션 ``` ~/Documents/Obsidian/MyVault 를 ainote 에 vault_clone 해줘 ``` → git push → ainote 가 hosting → 다른 기기에서 `vault_clone` 으로 받기. ### C. 다중 기기 동기화 맥미니에서 노트 작성 → `vault_sync` (자동 commit + push) → 맥북에서 `vault_sync` (pull) → 변경 도착. ## 폴더 구조 권장 ``` my-vault/ ├── daily/ # 일일 메모 │ └── 2026-05-07.md ├── projects/ # 프로젝트 노트 │ ├── ainote.md │ └── tennis-bracket.md ├── reference/ # 참고 자료 │ ├── books/ │ └── articles/ ├── meta/ # vault 자체 메타 │ └── README.md └── .obsidian/ # Obsidian 설정 (선택) ``` ## 충돌 해결 vault_sync 시 Git 3-way merge: - 같은 파일 같은 줄 양쪽 수정 → conflict marker → 수동 해결 - 다른 파일 / 다른 줄 → 자동 merge 자세히: [동기화 충돌 해결](/sync/conflicts). ## Obsidian 양방향 기존 Obsidian vault 를 ainote 에 등록하면: - 로컬 파일 시스템 = ainote vault git working tree - Obsidian 으로 편집 → `vault_sync` → ainote 반영 - Claude 가 ainote 로 편집 → `vault_sync` (pull) → Obsidian 새로고침에 보임 ::: tip Obsidian Sync 대체 Obsidian 의 유료 Sync 플러그인 ($5/mo) 대신 ainote vault 무료로 사용 가능. ::: ## Frontmatter 규칙 ainote 메모리는 frontmatter 표준: ```markdown --- name: my-note description: 한 줄 요약 type: project | feedback | reference | user ainote_sync: vault-name/path/in/vault.md # sync 대상이면 --- # 본문 ``` 이 형식 따르면 [메모리 4가지 타입](/memory/types) 자동 분류 가능. ## 다음 - [`vault_create` API](/reference/vault-create) - [`vault_clone` — 다른 기기에서 가져오기](/vault/clone) - [`vault_sync` — 양방향 동기화](/vault/sync) - [Git backend 자세히](/vault/git-backend) --- # vault_sync ::: tip ✅ 라이브 (서버) `vault_*` / `sync_*` 도구는 `api.ainote.dev` 서버에서 **동작합니다** — JSON-RPC `POST /api/mcp` 또는 [`@ainote/sdk`](/build/sdk) (`ai.vault.*` / `ai.sync.*`)로 호출하세요. (일부 vault 도구는 연결된 git-backed vault가 필요. 참고: `@ainote/mcp` npm 패키지 구버전엔 번들이 안 됐을 수 있으니 직접 JSON-RPC 또는 SDK 사용 권장.) ::: primary vault 의 파일을 read/write 한다 — `sync_push` / `sync_pull` 과 같은 계열의 래퍼다. **로컬 git 을 구동하지 않는다.** vault 파일 내용을 서버와 직접 주고받는다. ## 시그니처 ```json { "action": "list", "path": "daily/" } ``` | 파라미터 | 값 | 설명 | |---------|-----|------| | `action` | `list` \| `pull` \| `push` | 기본 `list` | | `path` | string | 상대 경로 필터, 또는 push 대상 파일 경로 | | `content` | string | (push) 파일 내용 | - **list** — path(또는 전체) 하위 파일 목록/메타 - **pull** — path 하위 파일 내용 받기 - **push** — path 파일에 content 쓰기. 큰 본문의 WAF 우회는 `content_b64` 또는 `content: "__B64__:..."` prefix (sync_push 와 동일) ## 예시 ```json // pull { "action": "pull", "path": "projects/ainote.md" } // push { "action": "push", "path": "daily/2026-05-07.md", "content": "# 오늘\n..." } ``` ## 로컬 git 과의 관계 vault 는 GitHub repo 다. 로컬에서 편집·버전관리를 하려면 [`vault_clone`](/vault/clone) 이 준 URL 로 clone 해서 **표준 git**(commit/push)을 쓴다. push 하면 GitHub webhook 이 ainote 를 재인덱싱한다. `vault_sync` 는 로컬 clone 없이 에이전트가 서버 경유로 파일을 직접 읽고 쓸 때 쓴다. ## 다음 - [`vault_clone` — clone URL 회수](/vault/clone) - [Git backend 자세히](/vault/git-backend) ---