| name | obsidian-ingest |
| description | 옵시디언 vault 자동 ingest — URL이 포함된 메시지에 능동 요청 동사 ("옵시디언에 저장", "옵시디언에 ingest", "vault에 ingest", "vault에 저장", "raw에 저장", "ingest해줘", "옵시디언에 넣어줘") 동반 시 자동 활성. insane-search Phase 0~3로 본문 추출 → wiki/inbox/ staging 저장 (Master 검토 영역) → 정제 엔트리 초안 → 사용자 승인 후 wiki/ persist. YouTube/Vimeo는 yt-dlp 자동 활용. 차단 사이트(LinkedIn/fmkorea/네이버 블로그) 도 우회 가능.
Korean active triggers: 옵시디언에 저장, 옵시디언에 ingest, vault에 ingest, vault에 저장, raw에 저장, ingest해줘, 옵시디언에 넣어줘. English active triggers: obsidian ingest, save to vault, archive to vault, ingest to obsidian.
Do NOT trigger for: - URL 없는 메시지 (능동 동사만 있어도 미트리거 — 가장 우선 가드) - 단순 URL 질문 ("이 URL 뭐야?") — WebFetch 처리 - "정리해줘" / "기록해줘" / "아카이브" 단독 — 기존 rules/contextual/obsidian.md
§3 제안 흐름 유지 (URL 동반 무관)
- "wiki" 단독 언급 — Confluence 영역 (mcp__wiki__* 사용) - 회사 도메인 URL (smilegate / confluence / wiki.smile* / jira) —
옵시디언 vault 미적용
- 부정 의향 표현 ("저장하지 마", "ingest 말고") — 능동 동사 매칭 후 부정어 검사 - 의향 표현 ("나중에 옵시디언에 넣어야겠다") — 능동 동사 매칭 안 됨 → 미트리거
|
Obsidian Ingest — 옵시디언 vault 자동 ingest
URL 한 줄 + 능동 요청 동사로 외부 자료를 옵시디언 vault에 자동 저장.
insane-search Phase 0~3 활용. wiki/inbox/ staging → Master 검토 → 정제 persist.
1. 자동 트리거 판정 로직
다음 조건을 모두 만족 시 활성화:
1. 사용자 메시지에 URL ≥ 1개 (필수 가드 — 가장 우선)
2. 능동 요청 동사 키워드 매칭:
- 한국어: "옵시디언에 저장", "옵시디언에 ingest", "vault에 ingest",
"vault에 저장", "raw에 저장", "ingest해줘", "옵시디언에 넣어줘"
- 영어: "obsidian ingest", "save to vault", "archive to vault",
"ingest to obsidian"
3. 부정어 부재 검사 ("하지 마", "말고", "안", "하지 않")
4. 회사 도메인 URL 부재 (§3 패턴)
조건 미충족 시 미트리거 — 다른 도구(WebFetch / WebSearch / 기존 obsidian.md §3) 처리.
2. 9단계 워크플로
[1] 트리거 판정 → 모두 PASS 시 진입
[2] URL 정규화 + 단축 URL follow + 다중 URL 분기
[3] insane-search 본문 추출 (Phase 0~3)
[3.5] is_content_valid 게이트 (단어 수 + 에러 시그니처)
[4] 중복 ingest 감지 (Glob)
[5] inbox/ 저장 (mkdir -p 선행)
[6] Master 검토 안내 + 정제 진행 게이트
[7] 정제 엔트리 초안 작성 (도메인 추정 + 풍부도 분기)
[8] 사용자 응답 분기 (y/n/수정 요청/모호)
[9] persist + index.md 갱신 + log.md 기록 (read-then-append)
9.1 index.md 갱신 (Step 5 INFO-2 시정 — 명시 추가)
1. 도메인 추정 결과 → wiki/index.md의 해당 섹션 찾기
- 매칭: `## <도메인 표시명>` (예: `## Reference (참조)`)
- 없으면: 새 섹션 생성 (Master 승인 후)
2. 해당 섹션 끝에 `- [[<topic>]] — <한 줄 요약>` 추가
3. 갱신 실패 시: "wiki/index.md 갱신 실패. 수동 갱신 부탁" 안내 (게이트 영향 없음)
3. 회사 URL 거부 패턴
COMPANY_URL_PATTERNS = [
r"wiki\.smilegate\.",
r"confluence\.",
r"\.smilegate\.",
r"jira\.",
]
매칭 시 거부 + 안내:
❌ 회사 도메인 URL은 옵시디언 vault 미적용 영역입니다.
- 회사 wiki: mcp__wiki__* MCP 사용
- 회사 Jira: mcp__jira__* MCP 사용
4. URL 정규화 + 다중 URL 분기
4.1 단축 URL follow
curl -sLI -o /dev/null -w "%{url_effective}" "<단축 URL>"
→ 최종 URL을 source, 단축 URL을 source_original 에 저장.
4.1.1 보안 가드 (Step 7 sec-code MAJOR-1 시정 — A10 SSRF 방어)
URL validation 의무:
- ✅ schema:
http:// / https:// 만 허용
- ❌ 차단:
file://, gopher://, ftp://, data:, javascript:
- ❌ private IP 차단:
127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.169.254 (AWS 메타데이터), ::1, fc00::/7
- ❌ IDN/punycode 정규화 후 회사 도메인 재검증 (MINOR-1 시정 — 키릴 문자 등 homograph 공격 방어)
from urllib.parse import urlparse
import ipaddress, idna
def validate_url(url):
p = urlparse(url)
if p.scheme not in ('http', 'https'):
raise ValueError(f"허용 안 된 schema: {p.scheme}")
host = p.hostname
try:
host_ascii = idna.encode(host).decode('ascii')
except Exception:
host_ascii = host
for pattern in COMPANY_URL_PATTERNS:
if re.search(pattern, host_ascii):
raise ValueError(f"회사 도메인 거부: {host_ascii}")
try:
ip = ipaddress.ip_address(host_ascii)
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
raise ValueError(f"private/internal IP 차단: {ip}")
except ValueError:
pass
return host_ascii
4.2 다중 URL (2~3개) 독립 배치 흐름
1. 각 URL 순차 추출 ([3]~[3.5] 반복)
2. 모든 inbox/ 파일 일괄 저장 ([5] 반복, mkdir 1회만)
3. 정제 초안 N개 미리보기 일괄 제시
4. 단일 승인 ("모두 persist" / "선택 persist" / "모두 폐기")
(wiki/CLAUDE.md §8은 raw→정제 단계용 — URL 추출 단계와 흐름 분리)
5. insane-search 호출 패턴
5.1 YouTube/Vimeo (Phase 0 special — yt-dlp)
PYTHONIOENCODING=utf-8 yt-dlp \
--dump-json --no-warnings --skip-download "<URL>"
→ stdout JSON 파싱:
title, channel / uploader, duration, view_count, upload_date
description (full text)
automatic_captions (157+ 언어 가능)
5.2 일반 URL (Phase 0~3 격자)
INSANE_DIR="$HOME/.claude/plugins/cache/gptaku-plugins/insane-search/0.4.1/skills/insane-search"
cd "$INSANE_DIR"
PYTHONIOENCODING=utf-8 python -m engine "<URL>" --trace
→ 출력 마지막 줄 파싱:
[engine] ok=True/False verdict=STRONG_OK/WEAK_OK/CHALLENGE/BLOCKED/UNKNOWN profile=... attempts=N
5.3 실패 시 재호출
PYTHONIOENCODING=utf-8 python -m engine "<URL>" --trace --json
→ 구조화 trace 파싱 → 실패 phase + 사유 추출.
5.4 회사 SSL 환경
settings.json env에 CURL_CA_BUNDLE / REQUESTS_CA_BUNDLE / SSL_CERT_FILE 영속화 (자동 적용 — 별도 export 불필요). 자세히: wiki/insane-search-fivetaku.md §6.
6. is_content_valid 게이트
def is_content_valid(raw_body):
if not raw_body or len(raw_body.strip()) < 100:
return False
word_count = len(raw_body.split())
if word_count < 100:
return False
error_signatures = [
'404 Not Found', 'Access Denied', 'Forbidden',
'로그인이 필요', '권한이 없', '페이지를 찾을 수 없',
'Page Not Found', 'Sign In', '회원만 볼 수 있'
]
body_lower = raw_body.lower()
for sig in error_signatures:
if sig.lower() in body_lower:
return False
return True
FAIL 시: 사용자에게 확인:
⚠️ 추출 결과 빈약 ({N} 단어 / 에러 시그니처 감지). 어떻게 할까요?
- (1) 그대로 inbox/에 저장
- (2) 폐기
- (3) 수동 복사 후 직접 inbox/에 붙여넣기
7. 중복 ingest 감지
GLOB_PATTERN="D:/Vibe Dev/AI Brain/wiki/inbox/*_<slug>.md"
GLOB_PATTERN_RAW="D:/Vibe Dev/AI Brain/wiki/raw/*_<slug>.md"
매칭 시 사용자 선택:
⚠️ 이미 ingest됨: [[<기존 파일>]]
- (1) 덮어쓰기 (기존 파일 교체)
- (2) 신규 _v2 (기존 보존 + 새 파일 _v2 suffix)
- (3) 정제 엔트리만 다시 작성 (inbox 재저장 안 함)
8. inbox/ 저장 표준
8.1 디렉토리 자동 생성
mkdir -p "D:/Vibe Dev/AI Brain/wiki/inbox/"
8.2 파일명 (ASCII-safe slug — 2026-05-06 운영 검증 후 시정)
[!warning] 결함 발견 + 시정 (task-134 운영 검증 #1, 2026-05-06)
이전 fallback re.match(r"[\w]+", title) 가 Python 3에서 한글 매칭 → slug에 한글 발생 위험.
ASCII 강제 + URL 영상 ID 활용 fallback 추가.
def make_slug(title, source_url=None):
"""
ASCII-safe slug 생성 (Windows + Git core.quotePath 호환).
Args:
title: 영상/페이지 title
source_url: URL — title에서 ASCII 키워드 추출 실패 시 영상 ID fallback
"""
import re
from datetime import datetime
ascii_title = title.encode('ascii', 'ignore').decode('ascii').strip()
if ascii_title:
slug = re.sub(r"[^a-zA-Z0-9]+", "-", ascii_title).strip("-").lower()
if slug:
return slug[:60]
if source_url:
m = re.search(r'(?:youtube\.com/watch\?v=|youtu\.be/)([a-zA-Z0-9_-]{11})', source_url)
if m:
return f"yt-{m.group(1)}"
path_seg = re.sub(r"[^a-zA-Z0-9]+", "-", source_url.split('/')[-1].split('?')[0]).strip("-").lower()
if path_seg:
return path_seg[:60]
first = re.match(r"[a-zA-Z0-9]+", title)
first_word = first.group() if first else "untitled"
ts = datetime.now().strftime("%H%M%S")
return f"{first_word}-{ts}"
검증 케이스 (2026-05-06 PASS):
make_slug("클로드 코드 ... (feat. Codex)", "https://www.youtube.com/watch?v=f0hcByvsyjU") → "feat-codex"
make_slug("중국이 홍콩을 어떻게 먹었는가", "https://youtu.be/AJ_lyBtWBOM") → "yt-AJ_lyBtWBOM"
make_slug("Hello World", None) → "hello-world"
make_slug("한글만 영상", None) → "untitled-<HHMMSS>"
make_slug("한글", "https://example.com/article-123") → "article-123"
→ 위치: wiki/inbox/<YYYY-MM-DD>_<slug>.md
8.3 frontmatter v2
---
source: <최종 URL — redirect 후>
source_original: <단축 URL 또는 입력 URL>
clipped_at: 2026-05-06T22:00:00+09:00
title: <추출된 제목 — 한글 OK>
author: <추출 가능 시 — 미상이면 생략>
tags: [auto-ingest, <도메인 추정>]
fetched_via: insane-search-<phase> | yt-dlp
fetched_phase: 0 | 1 | 2 | 3
verdict: STRONG_OK | WEAK_OK | (manual)
status: pending_review
inbox_added_at: 2026-05-06T22:00:00+09:00
---
8.4 본문
insane-search 추출 결과를 Markdown으로 저장. HTML 잔여물 제거.
9. Master 검토 안내 + 정제 게이트
inbox/ 저장 후 사용자에게:
✓ wiki/inbox/<slug>.md 저장 완료
- source: <URL>
- phase: <N>, verdict: <V>
- 단어 수: <count>
다음 진행을 선택해주세요:
- (1) 정제 엔트리 자동 작성 진행 (Step 7)
- (2) inbox/ 만 보존 (Master가 나중에 검토)
10. 정제 엔트리 초안 작성
10.1 도메인 자동 추정 (가중치)
DOMAIN_SIGNALS = {
'reference': ['cheat sheet', 'numbers', 'reference', 'guide', '레퍼런스',
'가이드', '체크리스트', 'API doc', 'documentation'],
'methodology': ['concept', 'philosophy', '개념', '원칙', 'methodology',
'principle', 'pattern', '패턴'],
'external-research': ['blog', 'medium.com', 'youtube.com', 'youtu.be',
'substack', 'twitter', 'x.com', 'reddit',
'hackernews', 'news.ycombinator'],
'cost-tracking': ['pricing', '가격', 'cost', 'token', '비용', '단가'],
'self-reference': ['anthropic.com/docs', 'claude code', 'claude.ai/docs'],
}
def estimate_domain(url, title, body_preview):
scores = {d: 0 for d in DOMAIN_SIGNALS}
for domain, signals in DOMAIN_SIGNALS.items():
for sig in signals:
if sig.lower() in url.lower():
scores[domain] += 3
if title and sig.lower() in title.lower():
scores[domain] += 2
if body_preview and sig.lower() in body_preview.lower():
scores[domain] += 1
if max(scores.values()) == 0:
return 'external-research'
sorted_d = sorted(scores.items(),
key=lambda x: (-x[1], list(DOMAIN_SIGNALS).index(x[0])))
return sorted_d[0][0]
10.2 풍부도 분기
def choose_structure(raw_body):
import re
word_count = len(raw_body.split())
has_sections = bool(re.findall(r'^##\s', raw_body, re.M))
has_code = '```' in raw_body
has_tables = '|' in raw_body and '---' in raw_body
score = 0
if word_count > 1000: score += 2
if word_count > 3000: score += 2
if has_sections: score += 1
if has_code: score += 1
if has_tables: score += 1
if score >= 4:
return '14_step_full'
elif score >= 2:
return 'medium'
else:
return 'minimum'
10.3 frontmatter (wiki/CLAUDE.md §4 표준)
---
topic: <topic-slug>
domain: <추정 결과>
source_type: from_url
confidence: 0.6
created_at: 2026-05-06
updated_at: 2026-05-06
last_verified_at: 2026-05-06
tags: [<도메인>, <추정 키워드>]
ref_files: []
ref_clips: ["wiki/inbox/<YYYY-MM-DD>_<slug>.md"]
status: draft
---
10.4 위키링크 자동 제안
Grep "키워드" wiki/ → 매칭 페이지 [[topic]] 자동 추가
11. 사용자 승인 게이트 (Karpathy 원칙)
11.1 응답 분기
def parse_user_response(response):
rl = response.strip().lower()
if rl in ('y', 'yes', '응', '네', '예', 'ok', '진행'):
return ('persist', None)
if rl in ('n', 'no', '아니', '아니오', '폐기', '취소'):
return ('discard', None)
if '도메인' in response or 'domain' in rl:
return ('modify_domain', extract_domain(response))
if '짧' in response or 'short' in rl:
return ('modify_size', 'minimum')
if '제목' in response or 'title' in rl or '파일명' in response:
return ('modify_filename', extract_topic(response))
return ('ambiguous', response)
11.2 모호 응답 재질문
응답이 'ambiguous':
"응답을 'y' (저장) / 'n' (폐기) / 구체 수정 요청 중 명확히 답변 부탁:
예) '도메인을 reference로', '더 짧게', '제목을 X로'"
자동 persist 절대 금지 (Karpathy 원칙).
12. log.md 기록 (read-then-append)
12.1 원자성 함수 (Step 5 INFO-1 시정 — 미존재 가드)
def append_to_log_md(entry):
import os
log_path = "D:/Vibe Dev/AI Brain/wiki/log.md"
if not os.path.exists(log_path):
os.makedirs(os.path.dirname(log_path), exist_ok=True)
with open(log_path, 'w', encoding='utf-8') as f:
f.write("# 옵시디언 Wiki — Activity Log\n\n")
with open(log_path, 'r', encoding='utf-8') as f:
existing = f.read()
new_content = existing.rstrip() + "\n" + entry + "\n"
with open(log_path, 'w', encoding='utf-8') as f:
f.write(new_content)
12.2 기록 포맷
- 2026-05-06T22:30 - **inbox** [[2026-05-06_<slug>]] (wiki/inbox/) — source: <URL_redacted>, phase: <N>, verdict: <V>
- 2026-05-06T22:32 - **생성** [[<topic>]] (domain: <X>, source_type: from_url, inbox_ref: [[2026-05-06_<slug>]]) — auto-ingest 정제
12.3 URL Redaction (Step 7 sec-code MINOR-2 시정 — PII/시크릿 방어)
log.md 기록 전 URL 쿼리스트링 redact 의무 (AI Brain repo GitHub push 대비):
SENSITIVE_QUERY_KEYS = [
'token', 'api_key', 'apikey', 'session', 'session_id', 'auth',
'access_token', 'password', 'pwd', 'secret', 'key', 'sig', 'signature'
]
def redact_url(url):
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
p = urlparse(url)
qs = parse_qs(p.query)
redacted = {}
for k, v in qs.items():
if any(s in k.lower() for s in SENSITIVE_QUERY_KEYS):
redacted[k] = ['<REDACTED>']
else:
redacted[k] = v
new_query = urlencode(redacted, doseq=True)
return urlunparse((p.scheme, p.netloc, p.path, p.params, new_query, p.fragment))
→ inbox/ frontmatter source 와 log.md 기록 모두 redact_url() 적용 후 저장.
참고: **inbox** 트리거는 wiki/CLAUDE.md §10에 미등록. 별도 task 로 §10 보강 권장 (운영상 영향 없음).
13. 에러 처리 매트릭스
| 에러 유형 | 감지 | 동작 |
|---|
| 네트워크 차단 | engine verdict=blocked 14 attempts | 사용자 보고 + Phase 3 Playwright 별도 권고 |
| 인증 필요 | engine blocked + URL이 SNS (twitter/linkedin login) | "수동 복사 후 inbox/에 직접 붙여넣기" 안내 |
| 추출 빈약 | is_content_valid() FAIL | 사용자 확인 (저장/폐기/수동) |
| 회사 URL | COMPANY_URL_PATTERNS 매칭 | 명시 거부 + 회사 MCP 안내 |
| 중복 ingest | Glob 매칭 | 사용자 선택 (덮어쓰기/신규 _v2/정제만) |
| insane-search 미설치 | ModuleNotFoundError | "task-139 plugin install 필요" 안내 |
| 회사 SSL 환경 미설정 | CertificateVerifyError | "settings.json env CURL_CA_BUNDLE 설정 필요" 안내 |
14. 모범 시나리오 3건
시나리오 A: YouTube 영상 ingest
입력: "https://youtu.be/WbzMtyyOQpM 옵시디언에 저장해줘"
[1] 트리거: ✅ URL + "옵시디언에 저장"
[2] URL 정규화: youtu.be → youtu.be (단축 X, 그대로)
[3] yt-dlp Phase 0:
title="바이브코딩 시대, 그래도 개발자가 알아야 할 숫자!"
channel="코딩하는기술사" / duration=1156s / view=17,434
[3.5] is_content_valid: PASS
[4] 중복 감지: 없음
[5] mkdir + inbox/2026-05-06_baibeukoding-sidae.md (slug ASCII-safe)
[6] Master 안내: "정제 엔트리 작성 진행할까요?"
사용자: "응"
[7] 도메인 추정: external-research (youtu.be 매칭)
풍부도: medium (자막 풍부)
[8] 사용자 승인: y
[9] wiki/<topic>.md persist + log.md **inbox** + **생성** 기록
시나리오 B: Medium 글 ingest
입력: "https://medium.com/@user/article-xyz vault에 ingest"
[1] 트리거: ✅ URL + "vault에 ingest"
[3] insane-search Phase 1 (Jina Reader): WEAK_OK
[5] inbox/ 저장
... 진행
시나리오 C: LinkedIn 차단 우회 ingest
입력: "https://linkedin.com/pulse/article-xxx 옵시디언에 저장"
[1] 트리거: ✅
[3] insane-search Phase 1 실패 → Phase 2 (curl_cffi identity spoofing) → STRONG_OK
JSON-LD articleBody 추출 성공
[5] inbox/ 저장 (verdict=STRONG_OK, fetched_phase=2)
... 진행
15. 연관 스킬
- [[wiki-lint]] — 옵시디언 vault 건강검진 (고아 페이지/깨진 링크 감지)
/sync — 회사 wiki/gdi 캐시 동기화 (옵시디언 vault 무관)
- [[insane-search-fivetaku]] (vault 가이드) — 본 스킬 핵심 의존
16. 운영 의무
- 자동 persist 금지 — 사용자 승인 후만 wiki/.md 저장 (Karpathy 원칙)
- inbox/ 안전 영역 — Master 검토 후 raw/ 이관 결정 (Master 직접 mv)
- 회사 자산 미적용 — 회사 URL 절대 거부
- 다른 PC sync — skills/obsidian-ingest/ AI Brain repo 자동 동기화 (installer v4)
- 운영 검증 의무 (Iron Law § 1) — 첫 사용 시 사용자 직접 검증 의무
위치: ~/.claude/skills/obsidian-ingest/SKILL.md
repo 동기화: D:/Vibe Dev/AI Brain/skills/obsidian-ingest/SKILL.md
의존: insane-search@gptaku-plugins (task-139 도입 완료)