diff --git a/.env.example b/.env.example
index 1d66f64..2f654d6 100644
--- a/.env.example
+++ b/.env.example
@@ -72,6 +72,7 @@ ASKOOSU_CHAT_MAX_REQUEST_BYTES=32768
ASKOOSU_RATE_LIMIT_STORE=postgres
ASKOOSU_CHAT_RATE_LIMIT_PER_MINUTE=60
ASKOOSU_CHAT_SESSION_RATE_LIMIT_PER_MINUTE=30
+ASKOOSU_IP_HASH_SALT=
ASKOOSU_FAQ_SEMANTIC_ROUTER_ENABLED=true
ASKOOSU_FAQ_SEMANTIC_DIRECT_MIN=0.88
ASKOOSU_FAQ_SEMANTIC_REWRITE_MIN=0.76
diff --git a/Dockerfile b/Dockerfile
index 6f5539f..3e54d5e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -24,6 +24,7 @@ RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
+COPY --from=builder --chown=nextjs:nodejs /app/content ./content
COPY --from=builder --chown=nextjs:nodejs /app/docs ./docs
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
diff --git a/content/blog/chatgpt-devspace-ram-saver.mdx b/content/blog/chatgpt-devspace-ram-saver.mdx
new file mode 100644
index 0000000..cc37628
--- /dev/null
+++ b/content/blog/chatgpt-devspace-ram-saver.mdx
@@ -0,0 +1,455 @@
+---
+title: "ChatGPT + DevSpace 긴 대화가 RAM을 먹어치울 때, Tampermonkey로 버텨낸 기록"
+date: 2026-08-20
+tags: ["ChatGPT", "DevSpace", "Tampermonkey", "Brave", "macOS", "메모리최적화"]
+description: "ChatGPT 웹에 DevSpace를 연결해 어디서든 코딩 지시를 내리기 시작한 뒤, 길어진 대화 한 탭이 수 GB의 RAM과 swap을 잡아먹는 문제를 겪었다. 브라우저 변경과 여러 userscript 실험 끝에 오래된 DOM을 직접 비우는 RAM Saver v5.0에 정착한 과정을 기록한다."
+slug: "chatgpt-devspace-ram-saver"
+published: true
+---
+
+## TL;DR
+
+- ChatGPT 웹에 DevSpace를 연결하고 나니 노트북을 열지 않아도 스마트폰에서 코딩 작업을 지시하고 결과를 확인할 수 있어서 작업 방식이 꽤 많이 바뀌었다.
+- 대신 한 대화를 오래 이어 갈수록 브라우저 탭의 메모리가 계속 커졌다. 내 환경에서는 한 세션이 1GB를 넘기는 것은 흔했고, 길어진 세션은 3~4GB 이상까지 올라갔다.
+- Brave, Orion, Edge 같은 다른 브라우저와 메모리 절약 설정도 시험했지만 내가 원했던 수준의 차이는 없었다.
+- 저장해 둔 긴 대화 HTML을 확인했을 때 DevSpace 관련 iframe뿐 아니라 SVG, 이미지, 코드블록, 버튼 같은 DOM이 대량으로 남아 있었다.
+- 결국 Tampermonkey userscript에서 **DevSpace iframe을 비우고, 오래된 메시지를 가상화하고, 화면 밖 이미지·SVG·코드블록을 정리하는 방식**으로 방향을 바꿨고, 이 방식으로 실제 사용 중 메모리 압박을 관리할 수 있게 됐다.
+
+이 글은 일반적인 ChatGPT 성능 벤치마크가 아니라 **내가 실제로 ChatGPT 웹 + DevSpace를 장시간 사용하는 환경에서 겪은 문제와 해결 과정**을 기록한 글이다.
+
+---
+
+## DevSpace를 연결하고 나니 작업 방식이 정말 편해졌다
+
+ChatGPT 브라우저에 DevSpace를 연결한 뒤 가장 크게 달라진 것은 장소의 제약이었다.
+
+예전에는 코드를 수정하려면 결국 노트북을 열어야 했다. 하지만 DevSpace를 붙여 두고 나서는 출퇴근 지하철에서도 스마트폰으로 작업을 지시할 수 있었다.
+
+예를 들면 이런 흐름이다.
+
+```text title="mobile-dev-workflow.txt"
+스마트폰에서 ChatGPT 열기
+ ↓
+작업 지시
+ ↓
+DevSpace가 집/사무실 Mac의 실제 저장소 접근
+ ↓
+코드 수정 / 테스트 / Git 작업
+ ↓
+스마트폰에서 결과 확인
+```
+
+노트북을 가방에서 꺼내지 않아도 된다는 것만으로도 생각보다 큰 차이가 있었다.
+
+특히 여러 저장소를 동시에 다루거나, CI 결과를 보고 후속 수정을 지시하거나, 이동 중에 간단한 검증을 맡길 때 편했다.
+
+그런데 대화를 오래 이어가기 시작하면서 다른 문제가 생겼다.
+
+## 문제는 대화가 길어질수록 브라우저 RAM이 너무 커진다는 것
+
+처음에는 그냥 "ChatGPT 웹이 조금 무겁네" 정도였다.
+
+그런데 한 세션에서 DevSpace 호출을 계속 쌓아가며 몇 시간씩 작업하니 상황이 달라졌다.
+
+내 환경에서는 ChatGPT 한 탭이 1GB를 넘어가는 일이 흔했고, 대화가 충분히 길어지면 **3~4GB 이상**을 잡아먹는 경우도 생겼다.
+
+메인 작업 머신으로 쓰던 MacBook Air는 RAM이 24GB였는데, 다른 앱을 함께 띄우고 긴 ChatGPT 세션 몇 개를 유지하면 **swap이 5GB 이상** 생기는 상황도 반복됐다.
+
+64GB RAM의 MacBook Pro로 옮기면 해결될 것 같았지만 그쪽에는 또 다른 문제가 있었다. PR 자동 리뷰를 위해 로컬 LLM을 함께 돌리고 있었기 때문에, 브라우저가 몇 GB씩 가져가는 것이 여전히 부담이었다.
+
+즉 단순히 "RAM이 많은 컴퓨터를 쓰면 된다"로 끝낼 문제가 아니었다.
+
+## Brave와 Orion으로 바꿔 봤지만 드라마틱하지 않았다
+
+처음에는 브라우저 엔진 문제라고 생각했다.
+
+검색하면서 Brave, Orion 같은 브라우저를 후보로 잡았다. Brave는 Chromium 계열이라 무거운 웹앱을 오래 돌릴 때 상대적으로 버티지 않을까 기대했고, Orion은 WebKit 기반의 가벼운 탭 관리가 장점이라고 해서 시험해 봤다. Edge에서도 같은 긴 대화를 열어 비교해 봤다.
+
+브라우저 자체의 메모리 절약 기능, 하드웨어 가속 설정 변경, 백그라운드 탭 정리 같은 것도 함께 시도했다.
+
+하지만 내 사용 패턴에서는 브라우저만 바꾼다고 문제가 사라지지는 않았다.
+
+긴 대화에 들어가면 결국 메모리가 크게 올라갔다.
+
+이때부터 "브라우저가 어떤 엔진인가"보다 **한 대화 안에 무엇이 계속 남아 있는가**를 보기 시작했다.
+
+## 저장된 대화 HTML을 보니 iframe만의 문제가 아니었다
+
+처음 Tampermonkey 스크립트를 만들 때는 DevSpace가 띄우는 iframe이 주범이라고 생각했다.
+
+그래서 초기 버전에서는 DevSpace나 workspace 관련 iframe을 찾아 `about:blank`로 바꾸고 내부를 비우는 방식부터 시도했다.
+
+그런데 저장한 긴 대화 HTML을 확인해 보니 한 시점에 다음처럼 많은 요소가 쌓여 있었다.
+
+| 요소 | 확인한 개수 |
+| --- | ---: |
+| iframe | 288 |
+| SVG | 734 |
+| 이미지 | 290 |
+| 코드블록 | 163 |
+| 버튼 | 1,293 |
+
+이 숫자는 ChatGPT의 일반적인 고정값이 아니다. **내가 길게 사용한 특정 세션을 저장해서 확인한 스냅샷**이다.
+
+중요했던 건 iframe 하나만 지운다고 끝날 구조가 아니었다는 점이다.
+
+툴 호출 결과, 아이콘 SVG, 이미지, syntax highlighting을 위한 수많은 `span`, 과거 메시지 DOM까지 한 페이지에 계속 남아 있었다.
+
+그래서 방향을 바꿨다.
+
+## v1.5 → v2.0 → v3.0, 그리고 결국 v5.0
+
+Tampermonkey 스크립트도 한 번에 완성된 것은 아니었다.
+
+처음에는 React 화면을 완전히 깨뜨리지 않으려고 DOM 노드를 무작정 `remove()`하는 대신 iframe의 `src`를 `about:blank`로 바꾸고 컨테이너 내부를 비우는 방식으로 시작했다.
+
+그 뒤에도 메모리가 충분히 내려가지 않아 더 공격적인 정리를 시도했고, 정리가 실제로 얼마나 일어나고 있는지 확인하려고 우측 하단에 카운터 뱃지도 붙였다.
+
+마지막에는 iframe만이 아니라 다음을 모두 대상으로 삼았다.
+
+- DevSpace / workspace 관련 iframe 비우기
+- 생각 블록과 툴 메시지 내부 비우기
+- 화면 밖 SVG 제거
+- 화면 밖 이미지 `src` 언로드
+- 화면 밖 코드블록의 syntax highlighting용 하위 `span` 제거
+- 오래된 메시지의 전체 DOM을 짧은 preview placeholder로 교체
+- 애니메이션과 transition 비활성화
+- MutationObserver로 새 DOM이 생길 때마다 다시 정리
+
+핵심은 마지막 항목 중 **오래된 메시지 가상화**였다.
+
+최근 메시지 몇 개만 온전하게 남기고, 이미 화면 위로 지나간 오래된 대화는 내용 전체를 들고 있지 않게 만드는 방식이다.
+
+## 최종적으로 정착한 ChatGPT RAM Saver v5.0
+
+아래가 마지막에 정착해서 사용한 버전이다.
+
+```javascript title="chatgpt-ram-saver-v5.user.js"
+// ==UserScript==
+// @name ChatGPT RAM Saver v5.0 (Total Memory Purge)
+// @namespace http://tampermonkey.net
+// @version 5.0
+// @description iframe뿐 아니라 SVG, 이미지, 코드블록, 이벤트리스너, 애니메이션까지 전부 정리하여 ChatGPT RAM을 극한까지 절약합니다.
+// @author You
+// @match https://chatgpt.com/*
+// @grant none
+// @run-at document-end
+// ==/UserScript==
+
+(function() {
+ 'use strict';
+
+ // ─── 상태 ───
+ let stats = { iframes: 0, svgs: 0, imgs: 0, codeblocks: 0, toolmsgs: 0, thoughts: 0 };
+ let isProcessing = false;
+ const KEEP_LAST_N_MESSAGES = 4; // 최근 N개 메시지만 풀 DOM 유지
+
+ // ─── 1. CSS 주입: 애니메이션 전체 비활성화 + GPU 메모리 절약 ───
+ const injectCSS = () => {
+ if (document.getElementById('ram-saver-css')) return;
+ const style = document.createElement('style');
+ style.id = 'ram-saver-css';
+ style.textContent = `
+ /* 숨길 요소 */
+ div[data-testid="thought-block"],
+ span.group\\/tool-message {
+ display: none !important;
+ height: 0 !important;
+ overflow: hidden !important;
+ }
+
+ /* 모든 CSS 애니메이션/트랜지션 비활성화 → GPU 메모리 절약 */
+ *, *::before, *::after {
+ animation-duration: 0s !important;
+ animation-delay: 0s !important;
+ transition-duration: 0s !important;
+ transition-delay: 0s !important;
+ }
+
+ /* 로딩 shimmer 비활성화 */
+ .loading-results-shimmer {
+ display: none !important;
+ background: none !important;
+ }
+
+ /* 가상화된(비워진) 메시지 placeholder */
+ [data-virtualized="true"] {
+ min-height: 40px;
+ max-height: 40px;
+ overflow: hidden !important;
+ opacity: 0.3;
+ }
+
+ /* 뱃지 */
+ #ram-saver-badge {
+ position: fixed !important;
+ z-index: 2147483647 !important;
+ bottom: 16px !important;
+ right: 16px !important;
+ padding: 6px 10px !important;
+ background: rgba(0,0,0,0.8) !important;
+ color: #00FF88 !important;
+ border-radius: 8px !important;
+ font: 11px/1.4 'SF Mono', Menlo, monospace !important;
+ pointer-events: none !important;
+ white-space: pre-line !important;
+ box-shadow: 0 2px 12px rgba(0,0,0,0.4) !important;
+ display: block !important;
+ visibility: visible !important;
+ opacity: 1 !important;
+ }
+ `;
+ (document.head || document.documentElement).appendChild(style);
+ };
+
+ // ─── 2. UI 뱃지 ───
+ const updateBadge = () => {
+ let b = document.getElementById('ram-saver-badge');
+ if (!b) {
+ b = document.createElement('div');
+ b.id = 'ram-saver-badge';
+ document.documentElement.appendChild(b);
+ }
+ const total = Object.values(stats).reduce((a, c) => a + c, 0);
+ b.textContent = `🧹 RAM Saver v5.0\niframe:${stats.iframes} svg:${stats.svgs} img:${stats.imgs}\ncode:${stats.codeblocks} tool:${stats.toolmsgs} think:${stats.thoughts}\n총 정리: ${total}개`;
+ };
+
+ // ─── 3. iframe 청소 ───
+ const purgeIframes = () => {
+ document.querySelectorAll('iframe:not([data-purged="true"])').forEach(frame => {
+ const t = (frame.getAttribute('title') || '').toLowerCase();
+ const s = (frame.getAttribute('src') || '').toLowerCase();
+
+ // 시스템 필수 iframe 보호 (display:none인 숨겨진 인증용)
+ if (frame.style.display === 'none' && !t && !s.includes('devspace')) return;
+
+ const isTarget = t.includes('devspace') || t.includes('ui://') ||
+ t.includes('workspace') || s.includes('devspace') ||
+ s.includes('workspace-app') || s.includes('saved_resource');
+
+ if (isTarget) {
+ try { frame.contentWindow && frame.contentWindow.stop(); } catch(e) {}
+ frame.removeAttribute('src');
+ frame.removeAttribute('srcdoc');
+ frame.src = 'about:blank';
+ frame.setAttribute('data-purged', 'true');
+
+ const wrapper = frame.closest('.no-scrollbar') ||
+ frame.closest('[class*="overflow-hidden"]') ||
+ frame.parentElement;
+ if (wrapper && wrapper !== document.body) {
+ wrapper.innerHTML = '';
+ wrapper.style.cssText = 'display:none!important;height:0!important;';
+ }
+ stats.iframes++;
+ }
+ });
+ };
+
+ // ─── 4. 화면 밖 SVG 제거 ───
+ const purgeSVGs = () => {
+ // 대화 영역 안의 SVG만 대상 (사이드바/입력창 SVG는 보호)
+ const chatArea = document.querySelector('[class*="react-scroll-to-bottom"]') ||
+ document.querySelector('main');
+ if (!chatArea) return;
+
+ chatArea.querySelectorAll('svg:not([data-purged="true"])').forEach(svg => {
+ // 화면 밖에 있는 SVG만 제거
+ const rect = svg.getBoundingClientRect();
+ if (rect.bottom < -200 || rect.top > window.innerHeight + 200) {
+ // 부모에 빈 placeholder 남기고 SVG 제거
+ svg.setAttribute('data-purged', 'true');
+ svg.innerHTML = '';
+ svg.style.display = 'none';
+ stats.svgs++;
+ }
+ });
+ };
+
+ // ─── 5. 화면 밖 이미지 언로드 ───
+ const purgeImages = () => {
+ const chatArea = document.querySelector('[class*="react-scroll-to-bottom"]') ||
+ document.querySelector('main');
+ if (!chatArea) return;
+
+ chatArea.querySelectorAll('img:not([data-purged="true"])').forEach(img => {
+ const rect = img.getBoundingClientRect();
+ if (rect.bottom < -300 || rect.top > window.innerHeight + 300) {
+ if (img.src && !img.getAttribute('data-original-src')) {
+ img.setAttribute('data-original-src', img.src);
+ }
+ img.removeAttribute('src');
+ img.removeAttribute('srcset');
+ img.setAttribute('data-purged', 'true');
+ img.style.cssText = 'height:20px!important;width:20px!important;';
+ stats.imgs++;
+ }
+ });
+ };
+
+ // ─── 6. 코드블록 구문 강조 제거 (span 노드 대량 절약) ───
+ const simplifyCodeBlocks = () => {
+ const chatArea = document.querySelector('[class*="react-scroll-to-bottom"]') ||
+ document.querySelector('main');
+ if (!chatArea) return;
+
+ chatArea.querySelectorAll('pre:not([data-simplified="true"])').forEach(pre => {
+ const rect = pre.getBoundingClientRect();
+ // 화면 밖의 코드블록만 단순화
+ if (rect.bottom < -200 || rect.top > window.innerHeight + 200) {
+ const codeEl = pre.querySelector('code');
+ if (codeEl) {
+ const plainText = codeEl.textContent; // span 다 벗기고 순수 텍스트만
+ codeEl.innerHTML = '';
+ codeEl.textContent = plainText;
+ }
+ pre.setAttribute('data-simplified', 'true');
+ stats.codeblocks++;
+ }
+ });
+ };
+
+ // ─── 7. 생각 블록 & 툴 메시지 비우기 ───
+ const purgeThoughtsAndTools = () => {
+ document.querySelectorAll('div[data-testid="thought-block"]:not([data-purged="true"])').forEach(el => {
+ el.innerHTML = '';
+ el.setAttribute('data-purged', 'true');
+ stats.thoughts++;
+ });
+ document.querySelectorAll('span.group\\/tool-message:not([data-purged="true"])').forEach(el => {
+ el.innerHTML = '';
+ el.setAttribute('data-purged', 'true');
+ stats.toolmsgs++;
+ });
+ };
+
+ // ─── 8. 오래된 메시지 가상화 (핵심!) ───
+ const virtualizeOldMessages = () => {
+ const messages = document.querySelectorAll('[data-message-id]:not([data-virtualized="true"])');
+ if (messages.length <= KEEP_LAST_N_MESSAGES) return;
+
+ // 최근 N개를 제외한 오래된 메시지의 내부를 비움
+ const toVirtualize = Array.from(messages).slice(0, messages.length - KEEP_LAST_N_MESSAGES);
+ toVirtualize.forEach(msg => {
+ const rect = msg.getBoundingClientRect();
+ // 화면 밖에 있을 때만 가상화
+ if (rect.bottom < -100) {
+ // 텍스트 요약만 남기고 내부 DOM 비우기
+ const role = msg.getAttribute('data-message-author-role') || 'unknown';
+ const preview = msg.textContent.substring(0, 80).trim();
+ msg.innerHTML = `
[${role}] ${preview}...
`;
+ msg.setAttribute('data-virtualized', 'true');
+ }
+ });
+ };
+
+ // ─── 메인 청소 함수 ───
+ const fullPurge = () => {
+ if (isProcessing) return;
+ isProcessing = true;
+ try {
+ purgeIframes();
+ purgeThoughtsAndTools();
+ purgeSVGs();
+ purgeImages();
+ simplifyCodeBlocks();
+ virtualizeOldMessages();
+ updateBadge();
+ } finally {
+ isProcessing = false;
+ }
+ };
+
+ // ─── MutationObserver (디바운스) ───
+ let timer = null;
+ const observer = new MutationObserver((muts) => {
+ if (muts.every(m => m.target.id === 'ram-saver-badge' || m.target.id === 'ram-saver-css')) return;
+ if (timer) clearTimeout(timer);
+ timer = setTimeout(fullPurge, 400);
+ });
+
+ // ─── 부팅 ───
+ injectCSS();
+ observer.observe(document.documentElement, { childList: true, subtree: true });
+
+ // 초기 실행 (페이지 로딩 직후 시차를 두고 3회)
+ setTimeout(fullPurge, 500);
+ setTimeout(fullPurge, 2000);
+ setTimeout(fullPurge, 5000);
+
+ // 백업: 5초마다 정기 청소 + 뱃지 복구
+ setInterval(() => {
+ fullPurge();
+ updateBadge();
+ }, 5000);
+
+ console.log('🚀 [ChatGPT RAM Saver v5.0] Total Memory Purge 가동!');
+})();
+```
+
+## 왜 이 방식이 효과가 있었나
+
+내 경우에는 브라우저를 바꾸는 것보다 **길어진 대화의 과거 UI를 계속 온전한 DOM으로 유지하지 않는 것**이 훨씬 직접적인 접근이었다.
+
+특히 DevSpace처럼 한 번의 대화에서 툴 호출이 많이 발생하는 사용 패턴에서는 단순 텍스트만 쌓이는 것이 아니다.
+
+각 호출의 UI, 아이콘, 상태 표시, 코드 결과, iframe, syntax highlighting 구조까지 함께 쌓일 수 있다.
+
+v5.0은 "브라우저 메모리 관리가 더 잘 되기를 기다리는 것" 대신, 내가 다시 볼 가능성이 낮은 과거 화면의 DOM을 아예 줄여 버린다.
+
+최근 네 개 메시지만 전체 DOM으로 유지하도록 한 것도 이 때문이다.
+
+```javascript title="important-setting.js"
+const KEEP_LAST_N_MESSAGES = 4;
+```
+
+이 숫자는 정답이 아니다. 과거 대화를 자주 위로 스크롤해서 확인한다면 8이나 12처럼 늘리는 편이 낫다.
+
+나는 장시간 코딩 세션에서 "지금 진행 중인 최근 문맥"을 보는 경우가 대부분이라 4로 두었다.
+
+## 대신 꽤 공격적인 스크립트다
+
+이 스크립트에는 분명한 대가가 있다.
+
+오래된 메시지의 내부 DOM을 직접 비우기 때문에, 위로 스크롤했을 때 원래의 완전한 메시지 UI를 그대로 다시 볼 수 없다. 이미지와 SVG도 unload하거나 비워 버리고, 코드블록의 syntax highlighting도 제거한다.
+
+또 ChatGPT 웹의 내부 `data-testid`, class 이름, 메시지 DOM 구조를 직접 참조한다. ChatGPT UI가 변경되면 selector가 더 이상 맞지 않거나 일부 기능이 깨질 수 있다.
+
+그래서 이 스크립트는 다음 전제로 쓰고 있다.
+
+1. 중요한 과거 대화는 필요하면 페이지를 새로고침하거나 별도로 기록한다.
+2. UI가 이상해지면 Tampermonkey 스크립트를 먼저 끄고 확인한다.
+3. ChatGPT 화면 구조가 바뀌면 selector를 다시 점검한다.
+4. 로그인·인증처럼 정체를 확실히 모르는 iframe은 무조건 삭제하지 않는다.
+
+즉 "모든 사람에게 추천하는 일반 최적화"라기보다, **한 세션에서 DevSpace 툴 호출을 아주 많이 쌓는 내 작업 방식에 맞춘 강제 가상화 스크립트**에 가깝다.
+
+## 결국 새 대화를 자주 여는 것이 가장 단순한 해결책이기도 하다
+
+이 과정에서 얻은 결론이 하나 더 있다.
+
+스크립트 없이 가장 확실하게 가벼운 상태를 유지하려면 한 세션을 끝없이 이어 가지 않는 것이 좋다.
+
+문제가 명확히 끝났다면 다음 작업은 새 대화에서 시작하고, 필요한 컨텍스트는 저장소의 문서나 Git 커밋, PR 설명에 남기는 방식이 훨씬 건강하다.
+
+하지만 실제 작업에서는 한 흐름을 끊기 어려울 때가 있다.
+
+특히 수정 → 테스트 → CI → 리뷰 → 재수정 과정을 한 대화에서 계속 이어갈 때는 세션을 유지하는 편이 편하다.
+
+그럴 때 이 userscript가 일종의 안전장치가 됐다.
+
+## 지금의 결론
+
+DevSpace를 ChatGPT에 연결한 장점은 포기하고 싶지 않았다.
+
+스마트폰에서 실제 개발 머신의 코드를 다루고, 출퇴근 중에도 작업을 이어갈 수 있다는 편리함은 너무 컸다.
+
+문제는 그 편리함을 오래 사용했을 때 브라우저 한 탭이 몇 GB씩 커지는 것이었다.
+
+Brave나 Orion으로 옮기는 것만으로는 내 환경에서 충분하지 않았다. 결국 내가 정착한 방법은 브라우저 선택이 아니라 **오래된 ChatGPT 화면을 적극적으로 버리는 것**이었다.
+
+Tampermonkey로 여러 버전을 만들며 실패하다가, iframe뿐 아니라 과거 메시지 DOM 전체까지 정리하는 v5.0으로 넘어간 뒤에야 24GB MacBook Air에서도 장시간 세션의 RAM과 swap 압박을 이전보다 관리 가능한 수준으로 유지할 수 있었다.
+
+아마 ChatGPT 웹의 DOM 구조는 계속 바뀔 것이다. 그래서 이 코드도 영구적인 해결책은 아니다.
+
+그래도 "AI 에이전트와 브라우저를 하루 종일 붙여 쓰는 시대에는 브라우저 탭 자체도 개발 환경의 일부로 관리해야 한다"는 점은 꽤 오래 남을 것 같다.
diff --git a/content/blog/devspace-team-onboarding.mdx b/content/blog/devspace-team-onboarding.mdx
new file mode 100644
index 0000000..5f234ea
--- /dev/null
+++ b/content/blog/devspace-team-onboarding.mdx
@@ -0,0 +1,1010 @@
+---
+title: "DevSpace 팀 온보딩: ChatGPT 웹에서 로컬 코드를 직접 다루기"
+date: 2026-08-13
+tags: ["DevSpace", "ChatGPT", "MCP", "ngrok", "Tailscale", "Cloudflare Tunnel", "팀협업"]
+description: "4명이 그대로 따라 할 수 있도록 DevSpace 설치부터 고정 HTTPS 주소, ChatGPT MCP 연결, 첫 저장소 테스트와 보안 규칙까지 10~15분 실전 절차로 정리한다."
+slug: "devspace-team-onboarding"
+published: true
+---
+
+## TL;DR
+
+- DevSpace는 ChatGPT가 내 컴퓨터의 실제 프로젝트 폴더를 읽고, 수정하고, 테스트 명령까지 실행하게 해 주는 self-hosted MCP 서버다.
+- DevSpace 자체가 Codex 사용량이나 ChatGPT 토큰 한도를 늘려 주는 것은 아니다. 대신 코드를 계속 복사해서 붙여넣는 대신 필요한 파일만 직접 읽게 할 수 있어서 컨텍스트 낭비와 작업 전환 비용을 줄이는 데 유용하다.
+- 내 Mac mini는 `127.0.0.1:7676`에서 DevSpace를 실행하고 `https://devspace.oosu.dev`라는 고정 Cloudflare Tunnel 주소를 붙여 사용하고 있다.
+- 개인 도메인이 없는 팀원은 **ngrok 무료 계정의 자동 할당 dev domain**을 기본 선택으로 추천한다. 계정에 할당된 `*.ngrok-free.app` 주소를 재사용할 수 있어서 집, 교육장, 카페처럼 네트워크가 바뀌어도 ChatGPT 쪽 MCP URL을 계속 바꿀 필요가 없다.
+- 이미 Tailscale을 쓰고 있다면 **Tailscale Funnel**도 좋은 대안이다. `machine-name.tailnet-name.ts.net` 형태의 예측 가능한 주소를 계속 사용할 수 있다.
+- Cloudflare Quick Tunnel은 계정이나 도메인 없이 바로 쓸 수 있지만 실행할 때마다 랜덤 `*.trycloudflare.com` 주소가 만들어지므로 이번 목적에는 적합하지 않다.
+
+이 글은 우리 4명 팀이 각자 자신의 컴퓨터에 DevSpace를 설치하고, ChatGPT 웹에서 실제 로컬 저장소를 직접 다루기 위한 팀 온보딩 문서다.
+
+Node와 Git이 이미 설치돼 있다면 10~15분을 목표로 한다.
+
+---
+
+## 먼저 중요한 것: DevSpace가 해결하는 문제와 해결하지 않는 문제
+
+현재 팀원들은 Codex Desktop으로 작업하면서 큰 저장소를 오래 다룰 때 사용량과 컨텍스트 압박을 자주 겪고 있다.
+
+DevSpace를 쓰는 이유를 여기서 정확히 구분해야 한다.
+
+DevSpace는 Codex 요금제의 사용량을 늘려 주거나 제한을 우회하는 도구가 아니다.
+
+대신 이런 작업을 줄여 준다.
+
+- 파일 내용을 매번 채팅에 붙여넣기
+- 저장소 구조를 매 세션 설명하기
+- AI가 수정한 코드를 다시 로컬에 복사하기
+- 테스트 명령을 사람이 대신 실행한 뒤 결과를 다시 붙여넣기
+- 병렬 작업 때문에 checkout이 꼬이는 문제
+
+연결이 끝난 뒤에는 ChatGPT가 승인된 프로젝트 폴더 안에서 필요한 파일을 직접 읽고, 검색하고, 수정하고, 테스트를 실행할 수 있다.
+
+즉 목표는 다음 구조다.
+
+```text title="architecture.txt"
+ChatGPT Web
+ ↓ HTTPS
+고정 공개 주소
+ ↓
+DevSpace MCP Server
+ ↓
+내가 허용한 로컬 프로젝트 폴더
+ ↓
+Git / 테스트 / 빌드 / 로컬 도구
+```
+
+---
+
+## 0. 시작 전에 ChatGPT 권한부터 확인하기
+
+설치보다 먼저 해야 할 일이 있다.
+
+2026년 8월 13일 기준 OpenAI 공식 안내에서는 **Developer mode와 full MCP 지원이 ChatGPT Business, Enterprise, Edu의 ChatGPT 웹에서 제공**되고 있다. Plugin Directory 자체는 더 넓은 플랜에서 보일 수 있지만, custom MCP를 만들고 실제 write/modify tool을 쓰는 권한은 플랜, 워크스페이스 설정, 사용자 역할에 따라 달라진다.
+
+제품 제공 범위와 UI는 베타 기간에 바뀔 수 있으므로 각자 계정에서 먼저 확인한다.
+
+### 왜 어떤 문서에는 Apps, 내 화면에는 Plugins라고 나오나?
+
+이 부분이 특히 헷갈린다.
+
+OpenAI는 **2026년 7월 9일 App Directory를 Plugin Directory로 변경**했다. 현재 Plugin은 workflow를 묶어 보여 주는 상위 단위이고, 실제 외부 MCP 서버에 연결되는 integration은 내부적으로 App으로 관리된다.
+
+그래서 다음 두 표현이 동시에 등장할 수 있다.
+
+```text title="plugin-vs-app.txt"
+현재 ChatGPT 화면
+Plugins / 새 플러그인
+
+OpenAI 공식 MCP 설정 문서
+Apps / Create
+```
+
+둘이 완전히 다른 기능이라는 뜻이 아니다. 우리 팀에서 실제로 DevSpace를 등록할 때는 **Plugins 메뉴의 새 플러그인 흐름**을 사용했고, 그 안에서 DevSpace MCP endpoint와 OAuth 인증을 연결했다.
+
+### Developer mode를 실제로 켜는 위치
+
+먼저 ChatGPT **웹**을 연다. 모바일 앱에서 설정하려고 하지 않는다.
+
+Business 워크스페이스의 관리자/소유자라면 다음 경로를 먼저 확인한다.
+
+```text title="chatgpt-menu.txt"
+Settings
+→ Apps
+→ Advanced Settings
+→ Developer mode
+```
+
+화면 구성에 따라 아래 custom app 생성 화면에 들어가면서 Developer mode를 활성화할 수도 있다.
+
+```text title="chatgpt-workspace-menu.txt"
+Workspace Settings
+→ Apps
+→ Create
+```
+
+Enterprise/Edu에서 일반 개발자 계정으로 사용하는 경우에는 관리자가 먼저 권한을 열어 줘야 한다.
+
+```text title="chatgpt-enterprise-admin.txt"
+관리자
+Workspace Settings
+→ Permissions & Roles
+→ Connected Data
+→ Developer mode / Create custom MCP connectors 허용
+```
+
+그다음 권한을 받은 사용자가 자신의 계정에서 켠다.
+
+```text title="chatgpt-enterprise-user.txt"
+사용자
+Settings
+→ Apps
+→ Advanced Settings
+→ Developer mode ON
+```
+
+메뉴가 안 보이면 DevSpace 설치 문제부터 의심하지 말고 먼저 **내 ChatGPT 플랜, 워크스페이스 역할, 관리자 정책**을 확인한다.
+
+쓰기 권한이 없는 계정에서는 DevSpace 설치 자체가 ChatGPT 플랜 제한을 우회해 주지 않는다.
+
+공식 안내:
+
+- [OpenAI - Developer mode and MCP apps in ChatGPT](https://help.openai.com/en/articles/12584461-developer-mode-and-full-mcp-connectors-in-chatgpt-beta)
+- [OpenAI - Plugins in ChatGPT and Codex](https://help.openai.com/en/articles/20001256-plugins-in-codexOpenAI)
+
+---
+
+## 1. 준비물 확인
+
+DevSpace 공식 요구사항은 다음과 같다.
+
+- Node `>=22.19 <27`
+- npm
+- Git
+- Bash
+- 외부에서 접근 가능한 HTTPS 주소
+
+macOS와 Linux는 일반적인 Bash 환경이면 된다.
+
+Windows는 PowerShell이나 `cmd.exe`만 있는 환경은 아직 지원 대상이 아니다. Git Bash, WSL, MSYS2, Cygwin Bash 중 하나를 준비한다. 가장 간단한 선택은 Git Bash다.
+
+먼저 확인한다.
+
+```bash title="terminal"
+node -v
+npm -v
+git --version
+bash --version
+```
+
+내 현재 Mac mini에서는 Node 24 계열과 DevSpace 1.0.7을 사용하고 있다.
+
+버전 숫자를 내 환경과 똑같이 맞출 필요는 없고 DevSpace 공식 범위 안이면 된다.
+
+---
+
+## 2. DevSpace 설치
+
+가장 단순한 설치 방법이다.
+
+```bash title="terminal"
+npm install -g @waishnav/devspace
+
+devspace --version
+```
+
+전역 설치가 싫다면 `npx`로도 사용할 수 있다.
+
+```bash title="terminal"
+npx @waishnav/devspace init
+npx @waishnav/devspace serve
+```
+
+이 글에서는 읽기 편하게 이후 명령을 `devspace` 기준으로 적는다.
+
+공식 문서:
+
+- [DevSpace Setup Guide](https://github.com/Waishnav/devspace/blob/main/docs/setup.md)
+
+---
+
+## 3. 제일 중요한 부분: 고정 HTTPS 주소 만들기
+
+처음 DevSpace를 쓸 때 내가 가장 불편했던 부분이 이 부분이었다.
+
+처음에는 임시 터널 주소를 사용했고, 집과 다른 장소를 오가거나 터널을 다시 띄울 때 주소가 달라졌다.
+
+DevSpace는 자신의 public URL을 기준으로 OAuth metadata와 Host allowlist를 만들기 때문에 공개 주소가 달라지면 설정과 인증 흐름을 다시 맞춰야 한다.
+
+그래서 지금 내 환경은 이렇게 고정해 두었다.
+
+```text title="my-current-devspace.txt"
+DevSpace local host: 127.0.0.1
+DevSpace local port: 7676
+Public base URL: https://devspace.oosu.dev
+
+Cloudflare Tunnel
+devspace.oosu.dev
+ → http://localhost:7676
+```
+
+이 방식의 장점은 집 Wi-Fi에서 교육장 Wi-Fi로 바뀌어 로컬 IP가 달라져도 외부에서 보는 주소는 계속 `devspace.oosu.dev`라는 것이다.
+
+하지만 팀원 모두가 개인 도메인을 가지고 있을 필요는 없다.
+
+### 추천 A. 개인 도메인이 없다면 ngrok 무료 dev domain
+
+우리 팀 기본값으로는 이 방법이 가장 간단하다.
+
+ngrok 무료 계정에는 자동으로 할당되는 dev domain 하나가 제공된다.
+
+예를 들면 이런 형태다.
+
+```text title="ngrok-domain.txt"
+https://example-name.ngrok-free.app
+```
+
+무료 플랜에서는 이름을 마음대로 고르거나 개인 도메인을 붙일 수는 없지만, 계정에 할당된 dev domain을 로컬 endpoint에 사용할 수 있다.
+
+### macOS
+
+```bash title="terminal"
+brew install ngrok
+
+ngrok config add-authtoken ""
+
+ngrok http 7676
+```
+
+### Windows
+
+ngrok 공식 Windows 다운로드 페이지에서 Microsoft Store, WinGet, Scoop 중 하나로 설치한 뒤 Git Bash에서 다음을 실행한다.
+
+```bash title="git-bash"
+ngrok config add-authtoken ""
+
+ngrok http 7676
+```
+
+처음 실행하면 다음처럼 HTTPS 주소가 출력된다.
+
+```text title="ngrok-output.txt"
+Forwarding
+https://example-name.ngrok-free.app
+→ http://localhost:7676
+```
+
+이 주소를 기억한다.
+
+중요한 포인트는 **DevSpace 설정에는 `/mcp`를 붙이지 않는 것**이다.
+
+```text title="correct-url.txt"
+DevSpace publicBaseUrl
+https://example-name.ngrok-free.app
+
+ChatGPT MCP endpoint
+https://example-name.ngrok-free.app/mcp
+```
+
+ngrok 무료 플랜 공식 설명:
+
+- [ngrok Free Plan Limits](https://ngrok.com/docs/pricing-limits/free-plan-limits)
+- [ngrok Share Localhost Quickstart](https://ngrok.com/docs/guides/share-localhost/quickstart)
+
+### 왜 Wi-Fi가 바뀌어도 괜찮나?
+
+ngrok agent는 집 공유기나 교육장 공유기에서 외부로 직접 포트를 여는 구조가 아니다.
+
+로컬 컴퓨터에서 ngrok cloud로 outbound 연결을 만들고, ngrok의 공개 주소가 그 연결을 `localhost:7676`으로 전달한다.
+
+따라서 네트워크가 바뀌었을 때 중요한 것은 로컬 IP 주소가 아니라 다음 두 가지다.
+
+- ngrok agent가 새 네트워크에서 인터넷에 다시 연결될 수 있는가
+- 같은 계정의 할당 dev domain을 계속 사용하는가
+
+이 두 조건을 만족하면 ChatGPT에 저장한 MCP URL은 그대로 둘 수 있다.
+
+### 컴퓨터 재부팅까지 자동 복구하고 싶다면
+
+ngrok은 macOS, Windows, Linux에서 OS background service로 설치할 수 있다.
+
+endpoint 설정을 `ngrok.yml`에 저장한 뒤 다음처럼 서비스로 등록할 수 있다.
+
+```bash title="terminal"
+ngrok service install --config /path/to/ngrok.yml
+ngrok service start
+```
+
+macOS에서는 `launchd`, Windows에서는 Windows Service로 등록된다.
+
+처음 팀 온보딩에서는 수동 `ngrok http 7676`으로 연결을 성공시킨 뒤, 필요할 때 background service까지 적용하는 것을 추천한다.
+
+공식 문서:
+
+- [ngrok Agent background service](https://ngrok.com/docs/agent)
+
+---
+
+## 4. 대안: 이미 Tailscale을 쓰고 있다면 Funnel
+
+Tailscale을 이미 설치해서 쓰는 팀원이라면 Funnel도 좋다.
+
+Funnel은 로컬 서비스를 인터넷에 HTTPS로 공개하면서 다음처럼 tailnet 기반 주소를 사용한다.
+
+```text title="tailscale-url.txt"
+https://my-mac.my-tailnet.ts.net
+```
+
+Tailscale 공식 문서에서도 Funnel DNS 이름을 predictable, stable한 주소로 설명한다.
+
+기본 흐름은 다음과 같다.
+
+```bash title="terminal"
+tailscale funnel 7676
+```
+
+환경에 따라 관리자 권한이 필요하면 다음처럼 실행한다.
+
+```bash title="terminal"
+sudo tailscale funnel 7676
+```
+
+출력 예시는 이런 형태다.
+
+```text title="tailscale-output.txt"
+Available on the internet:
+https://my-mac.my-tailnet.ts.net
+
+|-- / proxy http://127.0.0.1:7676
+```
+
+그다음 DevSpace에는 origin만 저장한다.
+
+```text title="tailscale-devspace.txt"
+publicBaseUrl
+https://my-mac.my-tailnet.ts.net
+
+ChatGPT endpoint
+https://my-mac.my-tailnet.ts.net/mcp
+```
+
+주의할 점도 있다.
+
+- Funnel은 MagicDNS와 HTTPS 설정이 필요하다.
+- Funnel용 tailnet policy 권한이 필요하다.
+- macOS는 Tailscale 배포 방식에 따라 CLI 사용 조건이 다르므로 공식 문서를 먼저 확인한다.
+
+공식 문서:
+
+- [Tailscale Funnel](https://tailscale.com/kb/1223/funnel)
+- [Tailscale Funnel examples](https://tailscale.com/docs/reference/examples/funnel)
+
+---
+
+## 5. Cloudflare Quick Tunnel은 왜 추천하지 않나?
+
+개인 도메인도 없고 계정도 만들기 싫다면 다음 명령으로 Cloudflare Quick Tunnel을 만들 수 있다.
+
+```bash title="terminal"
+cloudflared tunnel --url http://localhost:7676
+```
+
+문제는 주소다.
+
+```text title="quick-tunnel.txt"
+https://random-words.trycloudflare.com
+```
+
+Cloudflare 공식 문서상 Quick Tunnel은 실행할 때 랜덤 `*.trycloudflare.com` hostname을 만든다.
+
+프로세스를 새로 띄우면 URL이 달라질 수 있다.
+
+그러면 다음 값도 다시 맞춰야 한다.
+
+- DevSpace `publicBaseUrl`
+- ChatGPT custom MCP endpoint
+- 경우에 따라 OAuth 승인
+
+잠깐 테스트할 때는 매우 편하지만, 우리가 원하는 것은 "집에서도 교육장에서도 같은 ChatGPT MCP 연결을 계속 쓰는 것"이므로 팀 기본값으로는 맞지 않는다.
+
+공식 문서:
+
+- [Cloudflare Quick Tunnels](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/trycloudflare/)
+
+개인 도메인이 이미 있다면 내 환경처럼 Named Cloudflare Tunnel을 쓰는 것이 좋다.
+
+---
+
+## 6. DevSpace 초기화
+
+고정 HTTPS 주소를 만들었으면 이제 DevSpace를 초기화한다.
+
+```bash title="terminal"
+devspace init
+```
+
+설정 중 중요한 항목은 세 가지다.
+
+### Project roots
+
+ChatGPT가 접근할 수 있는 로컬 폴더다.
+
+내 환경에서는 여러 프로젝트 때문에 허용 범위를 넓게 둔 적이 있지만, 팀원들에게는 처음부터 좁게 잡는 것을 추천한다.
+
+예를 들어 팀 저장소가 여기 모여 있다면:
+
+```text title="project-root-example.txt"
+~/Documents/team-repos
+```
+
+이 상위 폴더만 허용한다.
+
+이렇게 하면 아래 저장소들을 모두 열 수 있다.
+
+```text title="team-repos.txt"
+team-repos/
+├── ontology_dashboard
+└── gen_data
+```
+
+다음처럼 홈 전체를 허용하는 것은 피한다.
+
+```text title="avoid-roots.txt"
+~
+/
+C:\
+```
+
+### Local port
+
+기본값을 그대로 쓴다.
+
+```text title="port.txt"
+7676
+```
+
+### Public base URL
+
+ngrok을 쓴다면:
+
+```text title="public-base-url.txt"
+https://example-name.ngrok-free.app
+```
+
+Tailscale이라면:
+
+```text title="public-base-url-tailscale.txt"
+https://my-mac.my-tailnet.ts.net
+```
+
+여기에는 `/mcp`를 붙이지 않는다.
+
+초기화가 끝나면 Owner password가 출력된다.
+
+이 비밀번호는 다음 위치에도 저장된다.
+
+```text title="devspace-auth-file.txt"
+~/.devspace/auth.json
+```
+
+이 파일과 Owner password는 각자 자기 컴퓨터에서만 관리한다.
+
+팀 Slack, Discord, Notion, GitHub Issue에 올리지 않는다.
+
+---
+
+## 7. DevSpace 실행과 진단
+
+먼저 설정을 진단한다.
+
+```bash title="terminal"
+devspace doctor
+```
+
+문제가 없다면 서버를 실행한다.
+
+```bash title="terminal"
+devspace serve
+```
+
+기본 로컬 endpoint는 다음과 같다.
+
+```text title="local-mcp.txt"
+http://127.0.0.1:7676/mcp
+```
+
+하지만 ChatGPT는 내 컴퓨터의 `127.0.0.1`에 직접 접근할 수 없으므로 실제 ChatGPT 설정에는 공개 HTTPS endpoint를 사용한다.
+
+---
+
+## 8. ChatGPT에 DevSpace 연결
+
+여기가 실제 연결에서 가장 중요한 부분이다.
+
+앞 단계에서 ChatGPT 웹의 Developer mode를 켰고, DevSpace 서버와 고정 HTTPS 주소가 모두 살아 있어야 한다.
+
+### 8-1. 현재 UI에서는 Plugins에서 시작한다
+
+우리 팀에서 실제로 연결할 때 확인한 흐름은 다음과 같다.
+
+```text title="chatgpt-plugin-create.txt"
+ChatGPT 웹 왼쪽 사이드바
+→ Plugins
+→ 새 플러그인(New plugin)
+```
+
+OpenAI 공식 도움말에는 같은 underlying MCP integration 설정을 `Settings → Apps → Create`라고 설명하는 부분도 있다. 2026년 7월 Plugin Directory 전환 이후 UI와 문서 용어가 같이 존재하고 있으므로, **현재 화면에 Plugins가 보이면 Plugins → 새 플러그인 흐름을 우선 사용**하면 된다.
+
+Plugins 메뉴가 보이지 않지만 Developer mode 권한이 있는 관리형 워크스페이스라면 아래 경로도 확인한다.
+
+
+```text title="chatgpt-create-app.txt"
+Settings
+→ Apps
+→ Create
+```
+
+또는 관리자 계정에서는:
+
+```text title="chatgpt-workspace-create-app.txt"
+Workspace Settings
+→ Apps
+→ Create
+```
+
+### 8-2. DevSpace MCP 정보를 입력한다
+
+새 플러그인 또는 custom app 생성 화면에서 이름은 팀에서 통일하면 찾기 편하다.
+
+```text title="app-name.txt"
+devspace-codex
+```
+
+Endpoint에는 이번에는 `/mcp`까지 붙인다.
+
+```text title="mcp-endpoint.txt"
+https://example-name.ngrok-free.app/mcp
+```
+
+내 환경이라면:
+
+```text title="my-mcp-endpoint.txt"
+https://devspace.oosu.dev/mcp
+```
+
+### 8-3. Authentication은 OAuth를 선택한다
+
+DevSpace는 MCP endpoint를 OAuth로 보호한다. 따라서 인증 방식 선택 항목이 나오면 **OAuth**를 선택한다.
+
+```text title="chatgpt-auth.txt"
+Authentication
+→ OAuth
+```
+
+이 OAuth는 Google/GitHub 로그인 같은 별도 서비스 계정 인증이 아니다. DevSpace가 제공하는 OAuth 승인 페이지가 열리고, 여기서 `devspace init` 때 발급된 **Owner password**로 이 ChatGPT 연결을 승인하는 구조다.
+
+Owner password는 다음 위치에도 저장돼 있다.
+
+```text title="devspace-owner-password.txt"
+~/.devspace/auth.json
+```
+
+파일 전체를 팀 채팅에 올리거나 password를 다른 팀원과 공유하면 안 된다. 네 명이 각각 자신의 DevSpace를 설치했다면 각자 서로 다른 Owner password를 사용한다.
+
+### 8-4. 실제 OAuth 승인과 Tool Scan
+
+현재 ChatGPT UI에서는 대략 다음 순서로 진행된다.
+
+1. 플러그인 이름을 `devspace-codex`로 입력한다.
+2. MCP endpoint에 `https://고정주소/mcp`를 입력한다.
+3. Authentication을 `OAuth`로 선택한다.
+4. `Scan Tools` 또는 연결 검사를 실행한다.
+5. 브라우저에 DevSpace OAuth 승인 페이지가 열리면 **내 Owner password**를 입력한다.
+6. ChatGPT로 돌아와 tool scan이 끝날 때까지 기다린다.
+7. `Create`, `Save` 또는 현재 UI의 생성 완료 버튼을 누른다.
+8. 새 채팅에서 `@devspace-codex`가 선택되는지 확인한다.
+
+정상이라면 ChatGPT가 DevSpace의 `open_workspace`, `read`, `apply_patch`, `exec_command` 같은 MCP tool을 스캔해 사용할 수 있게 된다.
+
+OAuth 승인이 끝났는데 다시 인증을 반복해서 요구한다면 다음부터 확인한다.
+
+```text title="oauth-troubleshooting.txt"
+1. DevSpace publicBaseUrl이 현재 고정 hostname과 같은가?
+2. ChatGPT endpoint 끝에 /mcp가 있는가?
+3. DevSpace 서버를 재시작한 뒤 OAuth client 상태가 바뀌지 않았는가?
+4. Owner password를 내 ~/.devspace/auth.json 기준으로 입력했는가?
+```
+
+DevSpace 공식 문서상 MCP client가 연결될 때 Owner password 승인 페이지가 열리며, OAuth redirect host와 public URL도 서버 설정의 일부다.
+
+여기서 가장 흔한 실수는 URL 두 개를 반대로 쓰는 것이다.
+
+```text title="url-rule.txt"
+DevSpace 설정
+https://host.example.com
+
+ChatGPT 설정
+https://host.example.com/mcp
+```
+
+DevSpace 공식 troubleshooting에도 별도 항목으로 나오는 실수다.
+
+---
+
+## 9. 처음에는 코드를 수정시키지 말고 연결만 확인하기
+
+첫 테스트는 읽기 전용으로 한다.
+
+ChatGPT 새 채팅에서 연결한 DevSpace app을 선택하거나 이름으로 호출한다.
+
+그리고 다음 프롬프트를 그대로 사용한다.
+
+```text title="first-test-prompt.txt"
+@devspace-codex
+
+내 로컬 팀 저장소를 checkout 모드로 열어줘.
+
+<내 절대경로>/team-repos/ontology_dashboard
+
+코드는 수정하지 말고 아래만 확인해줘.
+1. 현재 branch
+2. git status
+3. origin remote
+4. 최근 commit 3개
+```
+
+이 결과가 실제 로컬 저장소와 같으면 연결 성공이다.
+
+다음으로 `gen_data`도 같은 방식으로 확인한다.
+
+```text title="gen-data-test-prompt.txt"
+@devspace-codex
+
+<내 절대경로>/team-repos/gen_data 를 열고
+코드는 수정하지 말고 git status와 최근 commit만 확인해줘.
+```
+
+---
+
+## 10. 실제 팀 작업에서는 worktree를 기본으로 쓰기
+
+우리 팀은 같은 저장소를 여러 명, 여러 세션이 병렬로 작업할 일이 많다.
+
+기존 checkout branch를 직접 바꾸게 하면 다른 작업과 충돌하기 쉽다.
+
+그래서 실제 코드 작업을 맡길 때는 다음 문장을 기본으로 붙이는 것을 추천한다.
+
+```text title="worktree-prompt.txt"
+@devspace-codex
+
+기존 checkout branch는 변경하지 마.
+
+최신 origin 상태를 fetch한 뒤
+이번 작업용 독립 DevSpace worktree를 만들어서 작업해줘.
+
+작업 후에는
+1. 테스트
+2. git diff 확인
+3. git status 확인
+까지 진행해줘.
+```
+
+커밋과 push까지 맡길 작업이라면 그 부분을 명시적으로 추가한다.
+
+---
+
+## 11. Wi-Fi가 바뀌었는데 갑자기 연결이 안 될 때
+
+고정 주소를 제대로 썼다면 ChatGPT 설정부터 지우지 않는다.
+
+다음 순서로 확인한다.
+
+### 1. DevSpace 확인
+
+```bash title="terminal"
+devspace doctor
+```
+
+### 2. DevSpace 서버 확인
+
+```bash title="terminal"
+devspace serve
+```
+
+### 3. ngrok 확인
+
+```bash title="terminal"
+ngrok http 7676
+```
+
+출력되는 domain이 기존에 ChatGPT에 등록한 domain과 같은지 본다.
+
+### Tailscale 사용자라면
+
+```bash title="terminal"
+tailscale funnel status
+```
+
+고정 hostname이 그대로라면 ChatGPT app 설정은 그대로 둔다.
+
+---
+
+## 12. 정말 URL이 바뀐 경우
+
+Quick Tunnel 같은 임시 주소를 사용했다면 URL 자체가 바뀔 수 있다.
+
+그때는 DevSpace 설정부터 바꾼다.
+
+```bash title="terminal"
+devspace config set publicBaseUrl https://NEW-HOST.example.com
+```
+
+그리고 다시 실행한다.
+
+```bash title="terminal"
+devspace serve
+```
+
+ChatGPT custom MCP endpoint도 새 주소의 `/mcp`로 수정하거나 다시 만든다.
+
+```text title="new-chatgpt-url.txt"
+https://NEW-HOST.example.com/mcp
+```
+
+이 번거로움을 피하려고 처음부터 ngrok 할당 dev domain, Tailscale Funnel, Named Cloudflare Tunnel처럼 고정 hostname을 쓰는 것이다.
+
+---
+
+## 13. `unknown workspaceId`는 인증 실패가 아니다
+
+DevSpace를 재시작하거나 세션 상태가 바뀐 뒤 ChatGPT가 예전 `workspaceId`를 다시 사용하면 다음과 비슷한 오류가 날 수 있다.
+
+```text title="workspace-error.txt"
+unknown workspaceId
+```
+
+이 경우 Owner password나 tunnel 설정을 처음부터 다시 만들 필요는 없다.
+
+해당 프로젝트를 다시 `open_workspace` 하면 된다.
+
+`workspaceId`는 프로젝트 자체의 영구 ID가 아니라 작업 세션을 위한 식별자로 보는 편이 맞다.
+
+---
+
+## 14. 자주 막히는 문제
+
+### `devspace` command not found
+
+전역 npm bin 경로 문제일 수 있다.
+
+일단 `npx`로 확인한다.
+
+```bash title="terminal"
+npx @waishnav/devspace doctor
+```
+
+### Node version 오류
+
+```bash title="terminal"
+node --version
+```
+
+DevSpace 공식 범위인 `>=22.19 <27`인지 확인한다.
+
+### 403 또는 Host header 오류
+
+DevSpace의 `publicBaseUrl`과 실제 tunnel hostname이 같은지 본다.
+
+```bash title="terminal"
+devspace doctor
+```
+
+### ChatGPT에서 MCP tool scan 실패
+
+다시 한번 URL을 확인한다.
+
+```text title="scan-check.txt"
+잘못된 예
+DevSpace publicBaseUrl = https://host.example.com/mcp
+
+올바른 예
+DevSpace publicBaseUrl = https://host.example.com
+ChatGPT endpoint = https://host.example.com/mcp
+```
+
+### Owner password가 안 맞음
+
+자기 컴퓨터에서만 다음 파일을 확인한다.
+
+```text title="owner-password.txt"
+~/.devspace/auth.json
+```
+
+필요하면 setup을 다시 생성할 수 있다.
+
+```bash title="terminal"
+devspace init --force
+```
+
+공식 troubleshooting:
+
+- [DevSpace Gotchas](https://github.com/Waishnav/devspace/blob/main/docs/gotchas.md)
+
+---
+
+## 15. 보안 규칙은 꼭 지키기
+
+DevSpace는 단순한 파일 뷰어가 아니다.
+
+연결된 MCP 클라이언트는 승인된 workspace 안에서 파일을 수정할 수 있고, shell 명령도 실행할 수 있다.
+
+DevSpace 공식 문서도 이를 선택한 개발 머신에 대한 remote access로 취급하라고 안내한다.
+
+우리 팀에서는 다음 규칙을 지키는 것이 좋다.
+
+### Allowed root는 좁게
+
+좋은 예:
+
+```text title="good-roots.txt"
+~/Documents/team-repos
+~/work/Biz-CollabCraft
+```
+
+피해야 할 예:
+
+```text title="bad-roots.txt"
+~
+/
+C:\
+```
+
+### Owner password 공유 금지
+
+다음 파일은 Git에 올리지 않는다.
+
+```text title="private-file.txt"
+~/.devspace/auth.json
+```
+
+### `.env`, SSH key, cloud credential 주의
+
+가능하면 협업 저장소와 민감한 개인 credential 디렉터리를 같은 allowed root 안에 두지 않는다.
+
+### Shell 권한을 가볍게 보지 않기
+
+DevSpace file tool에는 경로 제한이 적용되지만 shell 명령은 로컬 사용자 권한으로 실행되는 강력한 기능이다.
+
+연결하는 ChatGPT 계정과 MCP 앱을 신뢰할 수 있는 코딩 파트너처럼 취급해야 한다.
+
+공식 보안 문서:
+
+- [DevSpace Security Model](https://github.com/Waishnav/devspace/security)
+
+---
+
+## 16. 우리 팀 권장 조합
+
+### 개인 도메인이 없는 대부분의 팀원
+
+```text title="recommended-default.txt"
+DevSpace
++ ngrok 무료 계정
++ 자동 할당 *.ngrok-free.app
++ ChatGPT custom MCP app
+```
+
+가장 설치가 빠르고 네트워크가 바뀌어도 공개 hostname을 유지하기 쉽다.
+
+### 이미 Tailscale을 쓰는 팀원
+
+```text title="recommended-tailscale.txt"
+DevSpace
++ Tailscale Funnel
++ *.ts.net
++ ChatGPT custom MCP app
+```
+
+### 개인 도메인을 이미 관리하는 경우
+
+```text title="recommended-cloudflare.txt"
+DevSpace
++ Named Cloudflare Tunnel
++ devspace.example.com
++ ChatGPT custom MCP app
+```
+
+이게 현재 내 Mac mini에서 사용하는 방식과 가장 가깝다.
+
+---
+
+## 17. 실제로 써보면 좋은 점: 코딩 화면이 한 컴퓨터에 묶이지 않는다
+
+DevSpace의 장점은 단순히 "ChatGPT가 로컬 파일을 읽는다"에서 끝나지 않는다.
+
+Codex Desktop만 사용할 때는 보통 코딩 작업과 대화가 그 컴퓨터의 데스크톱 앱을 중심으로 돌아간다. DevSpace를 ChatGPT 웹에 연결하면 **실제 코드는 내 컴퓨터에 그대로 두면서 ChatGPT 대화를 작업 제어 화면처럼 사용할 수 있다.**
+
+예를 들어 교육장에서 노트북으로 다음과 같이 작업할 수 있다.
+
+```text title="cross-device-example.txt"
+노트북의 DevSpace 서버
+ ↓
+ChatGPT 웹에서
+"PR 리뷰 반영 → 테스트 → commit → push까지 진행해줘"
+ ↓
+작업 결과와 대화가 ChatGPT 계정의 같은 conversation에 남음
+ ↓
+이동 중 휴대폰에서 같은 conversation을 열어
+진행 결과, diff 요약, 에러 내용을 확인
+ ↓
+후속 요구사항이나 다음 작업 방향을 바로 메시지로 남김
+```
+
+즉, 프로젝트 폴더를 휴대폰에 복사하거나 원격 데스크톱으로 작은 화면을 조작하지 않아도 **같은 ChatGPT 대화 흐름을 여러 기기에서 확인하고 이어갈 수 있다는 점**이 꽤 편하다.
+
+특히 우리 팀처럼 여러 PR과 저장소를 병렬로 다루는 경우에는 다음 장점이 있다.
+
+- 자리를 비운 뒤에도 휴대폰에서 작업 결과와 실패 원인을 확인하기 쉽다.
+- 팀 채팅에서 새 요구사항이 나오면 같은 ChatGPT conversation에 바로 추가해 다음 작업 컨텍스트로 남길 수 있다.
+- 데스크톱 Codex 앱의 특정 로컬 UI에만 작업 기록이 갇히지 않고 ChatGPT conversation을 기준으로 이어가기 쉽다.
+- 다음에 컴퓨터 앞에 돌아왔을 때 앞에서 무슨 작업을 했는지 다시 설명하는 비용이 줄어든다.
+
+다만 **2026년 8월 13일 현재 OpenAI 공식 안내에서 MCP apps는 web-only**다. Plugin Directory는 ChatGPT 웹과 데스크톱에서 사용할 수 있지만, 모바일 앱에서 DevSpace MCP tool을 직접 호출하는 흐름까지 공식 지원되는 것은 아니다.
+
+따라서 현재 기준으로는 이렇게 이해하는 게 정확하다.
+
+```text title="mobile-boundary.txt"
+모바일에서 잘하는 것
+→ 같은 ChatGPT conversation 열기
+→ 이전 작업 내용과 결과 확인
+→ 다음 요구사항과 수정 방향 전달
+→ 작업 지시를 대화 컨텍스트에 남기기
+
+현재 공식 지원을 전제로 하면 웹에서 하는 것
+→ DevSpace MCP를 실제 호출
+→ 로컬 파일 읽기/수정
+→ shell 명령 실행
+→ 테스트, commit, push
+```
+
+그리고 당연하지만 **DevSpace가 설치된 컴퓨터가 꺼져 있거나 잠자기 상태로 네트워크 연결이 끊기면 로컬 작업은 실행할 수 없다.** DevSpace 서버와 tunnel이 살아 있어야 ChatGPT 웹에서 그 컴퓨터에 접근할 수 있다.
+
+### macOS 팁: 이동 중에도 DevSpace 작업을 계속 돌리고 싶다면
+
+나는 MacBook을 들고 이동할 때도 DevSpace 작업이 중간에 끊기지 않도록 **시스템 잠자기를 임시로 비활성화**해서 사용한다. 배터리와 전원 연결 상태에 모두 적용하려면 Terminal에서 다음 명령을 실행한다.
+
+```bash title="macos-disable-sleep.sh"
+sudo pmset -a disablesleep 1
+```
+
+그다음 화면 밝기는 가능한 한 낮춰 두고, DevSpace 서버와 tunnel이 계속 실행되는 상태로 둔다. 이렇게 해 두면 이동 중에도 MacBook이 잠들어서 장시간 작업이나 테스트가 중간에 끊기는 상황을 줄일 수 있다.
+
+사용을 마쳤다면 **반드시 원래 설정으로 복구**한다.
+
+```bash title="macos-restore-sleep.sh"
+sudo pmset -a disablesleep 0
+```
+
+이 설정은 macOS 전체의 자동 잠자기를 막는 것이므로 상시 켜 두는 용도로 사용하지 않는 편이 좋다. 특히 다음은 꼭 주의한다.
+
+- 잠자기를 막아 두면 배터리 소모가 계속된다.
+- 빌드, 테스트, Docker 같은 작업이 돌아가는 동안에는 CPU 사용량과 발열이 올라갈 수 있다.
+- **작동 중인 MacBook을 백팩, 파우치, 서랍처럼 통풍이 거의 없는 밀폐 공간에 넣어 두지 않는다.** 덮개를 닫아 둘 때도 열이 빠질 수 있는 곳에 둔다.
+- 작업이 끝났거나 장시간 사용하지 않을 때는 `sudo pmset -a disablesleep 0`으로 되돌린다.
+
+즉, 이 방법은 "이동하는 동안 잠깐 원격 작업을 계속 돌려야 할 때" 쓰는 임시 운용 팁으로 생각하는 게 좋다.
+
+그래도 "로컬 코드는 내 컴퓨터에 두고, 작업 대화는 어디서든 확인한다"는 구조 자체가 기존 데스크톱 전용 코딩 에이전트와 비교했을 때 실사용에서 꽤 큰 차이다.
+
+---
+
+## 설치 완료 체크리스트
+
+- Node, npm, Git, Bash 버전 확인
+- DevSpace 설치
+- `devspace doctor` 통과
+- 고정 HTTPS hostname 확보
+- DevSpace `publicBaseUrl`에는 `/mcp` 없이 저장
+- ChatGPT endpoint에는 `/mcp` 포함
+- Owner password로 본인 연결만 승인
+- `ontology_dashboard` 읽기 전용 테스트 성공
+- `gen_data` 읽기 전용 테스트 성공
+- allowed roots가 필요한 협업 폴더 범위로 제한됨
+
+여기까지 되면 팀원이 Codex Desktop만 사용하지 않고, ChatGPT 웹에서도 자신의 실제 로컬 Git 저장소를 직접 연결해서 작업할 수 있는 기본 환경이 준비된 것이다.
+
+---
+
+## 참고 문서
+
+- [DevSpace GitHub](https://github.com/Waishnav/devspace)
+- [DevSpace Setup Guide](https://github.com/Waishnav/devspace/blob/main/docs/setup.md)
+- [DevSpace Troubleshooting Gotchas](https://github.com/Waishnav/devspace/blob/main/docs/gotchas.md)
+- [DevSpace Security Model](https://github.com/Waishnav/devspace/security)
+- [OpenAI Developer mode and MCP apps](https://help.openai.com/en/articles/12584461-developer-mode-and-full-mcp-connectors-in-chatgpt-beta)
+- [OpenAI Plugins in ChatGPT and Codex](https://help.openai.com/en/articles/20001256-plugins-in-codexOpenAI)
+- [ngrok Free Plan Limits](https://ngrok.com/docs/pricing-limits/free-plan-limits)
+- [ngrok Share Localhost Quickstart](https://ngrok.com/docs/guides/share-localhost/quickstart)
+- [ngrok Agent background service](https://ngrok.com/docs/agent)
+- [Tailscale Funnel](https://tailscale.com/kb/1223/funnel)
+- [Cloudflare Quick Tunnels](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/trycloudflare/)
diff --git a/content/blog/m1ddc-monitor-brightness.mdx b/content/blog/m1ddc-monitor-brightness.mdx
new file mode 100644
index 0000000..4fd3cc3
--- /dev/null
+++ b/content/blog/m1ddc-monitor-brightness.mdx
@@ -0,0 +1,231 @@
+---
+title: "한성 TFX173T 모니터 밝기 초기화 문제, m1ddc와 Swift로 완전 자동화하기"
+date: 2026-05-13
+tags: ["macOS", "m1ddc", "DDC/CI", "LaunchAgent", "Swift", "한성TFX173T", "모니터"]
+description: "한성 포터블 모니터를 연결할 때마다 백라이트 밝기가 1로 초기화되는 문제를 터미널 명령어로 해결하고, Swift 이벤트 기반 데몬으로 자동화한 과정을 기록한다."
+slug: "m1ddc-monitor-brightness"
+published: true
+---
+
+## TL;DR
+
+- 한성 TFX173T는 보조전원 없이 Type-C만 연결하면 OSD 설정이 초기화되도록 설계되어 있다.
+- `m1ddc` 툴로 DDC/CI를 통해 밝기를 소프트웨어로 제어할 수 있었다.
+- 폴링 방식 대신 `NSApplication.didChangeScreenParametersNotification` 이벤트를 감지하는 Swift 데몬과 LaunchAgent로 완전 자동화했다.
+
+## 한성 TFX173T 밝기가 매번 1로 초기화되는 문제
+
+한성 TFX173T 17인치 포터블 모니터를 맥북 프로에 Type-C로 연결해서 쓰고 있다. 화면 크기나 휴대성은 괜찮은데, 케이블을 뽑았다가 다시 꽂을 때마다 백라이트 밝기가 1로 초기화되는 문제가 있었다.
+
+처음에는 별일 아니라고 생각했는데, 매번 OSD 버튼을 눌러 밝기를 10까지 올리는 일이 반복되니까 꽤 귀찮았다. 그래서 공식 FAQ부터 찾아봤다.
+
+> TFX173T는 보조전원 없이 Type-C to C로 연결할 경우 OSD 설정 데이터가 초기화되도록 설계되어 있다. 이유는 보조전원 없이 연결 시 연결 기기에서 모니터가 출력될 수 있는 최소한의 전력만 사용하기 때문이다.
+
+공식 해결법은 보조전원을 먼저 연결한 뒤 화면 케이블을 연결하는 것이다. 분리할 때는 반대 순서로 해야 한다. 맞는 말이긴 한데, 매번 순서를 신경 써야 한다면 내가 해결하고 싶은 귀찮음이 그대로 남는다.
+
+그래서 방향을 바꿨다. 하드웨어 설정이 초기화된다면, 연결 직후 소프트웨어로 밝기를 다시 올리면 되지 않을까?
+
+## DDC/CI로 모니터 밝기를 제어할 수 있을까?
+
+모니터 밝기를 소프트웨어로 제어하는 표준 프로토콜로 DDC/CI가 있다. macOS에서 DDC/CI를 다루는 도구로 `ddcctl`이 많이 보였고, 먼저 이걸 시도했다.
+
+```bash title="terminal"
+brew install ddcctl
+ddcctl -d 1 -b 10
+```
+
+결과는 여기서 막혔다.
+
+```text title="ddcctl-output.txt"
+D: CGDisplay 67A24AAD-... (1080x1920 90°)
+D: CGDisplay 2FFCCCE9-... (1920x1080 0°)
+I: found 2 external displays
+E: Failed to parse WindowServer's preferences!
+E: Failed to acquire framebuffer device for display
+```
+
+디스플레이 자체는 인식됐다. 그런데 `Failed to acquire framebuffer device` 에러가 났다. macOS 최신 버전에서 SIP, 그러니까 시스템 무결성 보호가 강화되면서 `ddcctl`이 framebuffer에 직접 접근하는 방식이 막힌 것으로 보였다.
+
+여기서 `ddcctl`은 포기했다. 이 방향은 너무 낮은 레벨에서 막힌 느낌이었다.
+
+## m1ddc set luminance가 안 되는 것처럼 보인 이유
+
+다음으로 시도한 도구는 `m1ddc`였다. `m1ddc`는 `ddcctl`과 달리 framebuffer 접근 없이 I2C로 직접 DDC 통신을 하는 도구라 SIP 문제를 우회할 수 있다. 특히 Apple Silicon Mac과 USB-C DisplayPort Alt Mode 환경에 맞춰져 있다.
+
+```bash title="terminal"
+brew install m1ddc
+m1ddc get luminance
+# 110
+m1ddc set luminance 80
+# 80
+m1ddc get luminance
+# 110
+```
+
+이상했다. `set` 명령은 전달되는 것처럼 보이는데, 다시 `get`으로 확인하면 110으로 돌아왔다. 처음에는 이 모니터가 luminance VCP 코드, 즉 `0x10`을 외부에서 쓰는 걸 무시하는 줄 알았다.
+
+그런데 문제는 모니터가 아니라 내가 보고 있던 디스플레이 번호였다.
+
+## m1ddc display list detailed로 디스플레이 번호 확인하기
+
+연결된 디스플레이 목록을 자세히 확인했다.
+
+```bash title="terminal"
+m1ddc display list detailed
+```
+
+```text title="display-list.txt"
+[1] (null) (37D8832A-...) ← 맥북 내장 디스플레이
+[2] TFX173T (67A24AAD-...) ← 한성 모니터
+[3] LG IPS FULLHD (2FFCCCE9-) ← LG 모니터
+```
+
+여기서 중요한 사실을 발견했다. 맥북 프로의 외장 디스플레이로 연결되어 있다 보니, 내가 생각한 `display 1`과 실제 제어 대상이 엇갈리고 있었다. 디스플레이 번호를 명시하지 않으면 `display list`의 첫 번째 외장 디스플레이처럼 동작하는 구간도 있어서 더 헷갈렸다.
+
+또 하나 확인한 점은 LG 모니터였다. LG 모니터는 HDMI로 연결되어 있었고, `m1ddc`가 제어하지 못했다. `m1ddc` 공식 문서에도 내장 HDMI 포트로 연결된 디스플레이는 지원하지 않는다고 되어 있다. 내 환경에서는 DDC/CI 제어가 USB-C DisplayPort Alt Mode로 연결된 한성 모니터에서만 동작했다.
+
+디스플레이 번호를 정확히 지정해서 다시 시도했다.
+
+```bash title="terminal"
+m1ddc display 2 set luminance 100
+m1ddc display 2 get luminance
+# 100
+```
+
+됐다. 이번에는 진짜로 반영됐다.
+
+최대 밝기도 확인했다.
+
+```bash title="terminal"
+m1ddc display 2 max luminance
+# 100
+```
+
+결론은 간단했다. 한성 TFX173T는 `m1ddc display 2 set luminance 100`으로 제어하면 된다.
+
+## LaunchAgent 폴링 방식은 왜 별로였나?
+
+이제 남은 일은 자동화였다. 케이블을 연결할 때마다 내가 직접 명령을 치면 해결이라고 부르기 민망하다. 처음에는 LaunchAgent의 `StartInterval`로 주기 실행하는 방식을 시도했다.
+
+```xml title="com.user.monitor-brightness.plist"
+StartInterval
+30
+```
+
+30초마다 아래 명령을 실행하는 방식이다.
+
+```bash title="terminal"
+m1ddc display 2 set luminance 100
+```
+
+작동은 했다. 하지만 바로 마음에 안 드는 점들이 보였다.
+
+1. 지연 시간이 들쑥날쑥하다. 케이블 연결 직후 타이머가 거의 끝나 있으면 금방 적용되지만, 타이머가 막 리셋됐으면 최대 30초 이상 기다려야 한다.
+2. 모니터가 연결되어 있든 없든 30초마다 프로세스를 실행한다.
+3. 이벤트가 분명히 있는 작업을 폴링으로 처리하는 느낌이 지저분했다.
+
+`sleep`도 줄여봤다. `sleep 3`은 괜찮았는데 `sleep 2`로 줄이면 DDC 통신이 준비되기 전에 명령이 날아가서 적용이 안 됐다. 내 환경에서는 연결 후 약 3초가 현실적인 최솟값이었다.
+
+## Swift 이벤트 기반 데몬으로 바꾸기
+
+macOS는 디스플레이가 연결되거나 해제될 때 `NSApplication.didChangeScreenParametersNotification` 노티피케이션을 발생시킨다. 이걸 감지하면 폴링 없이 이벤트 기반으로 밝기 명령을 실행할 수 있다.
+
+다음 Swift 스크립트를 작성했다.
+
+```swift title="monitor-brightness.swift"
+import Cocoa
+
+func setMonitorBrightness() {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
+ let task = Process()
+ task.launchPath = "/bin/sh"
+ task.arguments = ["-c", "/opt/homebrew/bin/m1ddc display 2 set luminance 100"]
+ try? task.run()
+ }
+}
+
+NotificationCenter.default.addObserver(
+ forName: NSApplication.didChangeScreenParametersNotification,
+ object: nil,
+ queue: .main
+) { _ in
+ setMonitorBrightness()
+}
+
+setMonitorBrightness()
+NSApplication.shared.run()
+```
+
+컴파일은 이렇게 했다.
+
+```bash title="terminal"
+swiftc ~/monitor-brightness.swift -o ~/monitor-brightness
+```
+
+테스트는 단순했다. 케이블을 뽑았다가 다시 꽂았다. 약 3~4초 후 밝기가 자동으로 100까지 올라왔다. 됐다. 이벤트가 없을 때는 아무것도 하지 않고, 연결 이벤트가 생겼을 때만 반응한다.
+
+## LaunchAgent로 부팅 시 자동 실행하기
+
+마지막으로 Swift 데몬을 macOS 로그인 시 자동 실행되도록 LaunchAgent에 등록했다.
+
+기존 폴링 방식 LaunchAgent를 먼저 제거했다.
+
+```bash title="terminal"
+launchctl unload ~/Library/LaunchAgents/com.user.monitor-brightness.plist
+rm ~/Library/LaunchAgents/com.user.monitor-brightness.plist
+```
+
+새 LaunchAgent를 만들었다.
+
+```bash title="terminal"
+cat > ~/Library/LaunchAgents/com.user.monitor-brightness.plist << 'EOF'
+
+
+
+
+ Label
+ com.user.monitor-brightness
+ ProgramArguments
+
+ /Users/gabriel/monitor-brightness
+
+ RunAtLoad
+
+ KeepAlive
+
+
+
+EOF
+
+launchctl load ~/Library/LaunchAgents/com.user.monitor-brightness.plist
+```
+
+설정 의미는 이렇다.
+
+- `RunAtLoad: true`: 로그인 시 자동 실행
+- `KeepAlive: true`: 데몬이 종료되면 자동 재시작
+- `StartInterval` 없음: 폴링 없이 이벤트 기반으로만 동작
+
+등록 상태도 확인했다.
+
+```bash title="terminal"
+launchctl list | grep monitor-brightness
+# - 0 com.user.monitor-brightness
+```
+
+여기서 `0`은 에러 없이 정상 실행 중이라는 뜻이다.
+
+## 정리: Type-C만 꽂아도 5초 안에 밝기 복구
+
+결과적으로 보조전원 없이 Type-C만 꽂아도 5초 이내에 자동으로 밝기 100으로 올라오게 됐다. 처음에 원했던 "연결하면 알아서 밝아지는" 동작이 완성됐다.
+
+이번 삽질을 정리하면 이렇다.
+
+- `ddcctl`은 SIP 때문에 framebuffer 접근에서 실패했다.
+- `m1ddc`는 동작했지만, 디스플레이 번호를 잘못 이해해서 처음에는 반영이 안 되는 것처럼 보였다.
+- `m1ddc display list detailed`로 TFX173T가 `display 2`임을 확인했다.
+- LG 모니터는 HDMI 연결이라 이 환경에서는 `m1ddc` 제어 대상이 아니었다.
+- LaunchAgent 폴링 방식은 작동했지만 지연과 낭비가 있었다.
+- Swift 이벤트 기반 데몬으로 바꾸니 연결 직후 일정하게 밝기가 복구됐다.
+
+
diff --git a/db/migrations/009_create_ask_events.sql b/db/migrations/009_create_ask_events.sql
index dba876e..ca0919e 100644
--- a/db/migrations/009_create_ask_events.sql
+++ b/db/migrations/009_create_ask_events.sql
@@ -7,6 +7,7 @@ CREATE TABLE IF NOT EXISTS ask_events (
language text CHECK (language IS NULL OR language IN ('ko', 'en')),
question text NOT NULL DEFAULT '',
question_redacted text NOT NULL DEFAULT '',
+ answer_preview text,
normalized_intent text,
answer_mode text NOT NULL DEFAULT 'unknown'
CHECK (answer_mode IN ('direct_cache', 'rag', 'fallback', 'smalltalk', 'safety', 'unknown')),
@@ -23,7 +24,18 @@ CREATE TABLE IF NOT EXISTS ask_events (
utm_source text,
utm_medium text,
utm_campaign text,
+ ip_hash text,
+ cf_ray text,
country text,
+ geo_city text,
+ geo_region text,
+ geo_region_code text,
+ geo_postal_code text,
+ geo_timezone text,
+ geo_latitude numeric(9, 6),
+ geo_longitude numeric(9, 6),
+ user_agent_hash text,
+ accept_language text,
device_type text,
browser text,
os text
diff --git a/db/migrations/010_add_ask_event_answer_preview.sql b/db/migrations/010_add_ask_event_answer_preview.sql
new file mode 100644
index 0000000..07f007d
--- /dev/null
+++ b/db/migrations/010_add_ask_event_answer_preview.sql
@@ -0,0 +1,2 @@
+ALTER TABLE ask_events
+ ADD COLUMN IF NOT EXISTS answer_preview text;
diff --git a/db/migrations/011_add_visitor_events_and_identity_context.sql b/db/migrations/011_add_visitor_events_and_identity_context.sql
new file mode 100644
index 0000000..87af3a7
--- /dev/null
+++ b/db/migrations/011_add_visitor_events_and_identity_context.sql
@@ -0,0 +1,65 @@
+CREATE EXTENSION IF NOT EXISTS pgcrypto;
+
+ALTER TABLE ask_events
+ ADD COLUMN IF NOT EXISTS ip_hash text,
+ ADD COLUMN IF NOT EXISTS cf_ray text,
+ ADD COLUMN IF NOT EXISTS geo_city text,
+ ADD COLUMN IF NOT EXISTS geo_region text,
+ ADD COLUMN IF NOT EXISTS geo_region_code text,
+ ADD COLUMN IF NOT EXISTS geo_postal_code text,
+ ADD COLUMN IF NOT EXISTS geo_timezone text,
+ ADD COLUMN IF NOT EXISTS geo_latitude numeric(9, 6),
+ ADD COLUMN IF NOT EXISTS geo_longitude numeric(9, 6),
+ ADD COLUMN IF NOT EXISTS user_agent_hash text,
+ ADD COLUMN IF NOT EXISTS accept_language text;
+
+CREATE INDEX IF NOT EXISTS ask_events_ip_hash_idx
+ ON ask_events (ip_hash);
+
+CREATE TABLE IF NOT EXISTS visitor_events (
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
+ created_at timestamptz NOT NULL DEFAULT now(),
+ session_id text NOT NULL DEFAULT '' CHECK (char_length(session_id) <= 128),
+ ip_hash text,
+ event_type text NOT NULL DEFAULT 'page_view' CHECK (event_type IN ('page_view')),
+ path text,
+ referrer text,
+ utm_source text,
+ utm_medium text,
+ utm_campaign text,
+ cf_ray text,
+ country text,
+ geo_city text,
+ geo_region text,
+ geo_region_code text,
+ geo_postal_code text,
+ geo_timezone text,
+ geo_latitude numeric(9, 6),
+ geo_longitude numeric(9, 6),
+ user_agent_hash text,
+ accept_language text,
+ device_type text,
+ browser text,
+ os text,
+ screen_width integer CHECK (screen_width IS NULL OR screen_width >= 0),
+ screen_height integer CHECK (screen_height IS NULL OR screen_height >= 0),
+ viewport_width integer CHECK (viewport_width IS NULL OR viewport_width >= 0),
+ viewport_height integer CHECK (viewport_height IS NULL OR viewport_height >= 0),
+ client_timezone text,
+ client_language text
+);
+
+CREATE INDEX IF NOT EXISTS visitor_events_created_at_idx
+ ON visitor_events (created_at DESC);
+CREATE INDEX IF NOT EXISTS visitor_events_session_id_idx
+ ON visitor_events (session_id);
+CREATE INDEX IF NOT EXISTS visitor_events_ip_hash_idx
+ ON visitor_events (ip_hash);
+CREATE INDEX IF NOT EXISTS visitor_events_path_idx
+ ON visitor_events (path);
+CREATE INDEX IF NOT EXISTS visitor_events_referrer_idx
+ ON visitor_events (referrer);
+CREATE INDEX IF NOT EXISTS visitor_events_country_idx
+ ON visitor_events (country);
+CREATE INDEX IF NOT EXISTS visitor_events_utm_source_idx
+ ON visitor_events (utm_source);
diff --git a/docs/analytics.md b/docs/analytics.md
index 6518d28..e33d5b3 100644
--- a/docs/analytics.md
+++ b/docs/analytics.md
@@ -1,10 +1,11 @@
# AskOosu Analytics And Question Logging
-Last updated: 2026-05-13
+Last updated: 2026-05-19
## Storage Choice
-AskOosu uses the existing PostgreSQL database for custom AskOosu question logs.
+AskOosu uses the existing PostgreSQL database for custom AskOosu visitor and
+question logs.
This is the most natural option for this codebase because the project already
has:
@@ -19,11 +20,23 @@ pattern is already sufficient.
## What Is Logged
+`visitor_events` stores page-view and acquisition signals:
+
+- anonymous `session_id`
+- `ip_hash`, generated server-side with HMAC-SHA256
+- path, referrer without query string, UTM values
+- Cloudflare country and optional city/region/timezone/lat-lon headers
+- Cloudflare Ray ID
+- user-agent hash plus coarse device/browser/OS labels
+- screen, viewport, browser timezone, and browser language
+
`ask_events` stores privacy-conscious product-quality signals:
- anonymous `session_id`
+- `ip_hash`, so question behavior can be correlated with visits without raw IP storage
- language
- sanitized `question` and `question_redacted`
+- sanitized `answer_preview`, a short redacted response excerpt for quality review
- normalized intent
- answer mode: `direct_cache`, `rag`, `fallback`, `smalltalk`, `safety`, `unknown`
- confidence
@@ -32,9 +45,19 @@ pattern is already sufficient.
- latency
- optional user feedback value
- page path, referrer without query string, UTM values
-- country code, device type, browser, OS
+- country code, optional city/region fields, device type, browser, OS
-Raw IP addresses and precise geolocation are not stored.
+Raw IP addresses are not stored. `ip_hash` is intended for repeat-visitor
+analysis, not identity proof. Set a stable production-only salt so the same IP
+hashes consistently over time:
+
+```bash
+ASKOOSU_IP_HASH_SALT=
+```
+
+City/region fields depend on Cloudflare request headers. In Cloudflare, enable
+visitor location headers if city/region-level reporting is needed. Location from
+IP is approximate and may reflect VPNs, carriers, or company networks.
## Cloudflare Web Analytics
@@ -80,6 +103,92 @@ WHERE created_at < now() - interval '180 days';
## Useful Read Queries
+Daily visitor stats:
+
+```sql
+SELECT
+ date_trunc('day', created_at) AS day,
+ count(*) AS page_views,
+ count(DISTINCT session_id) AS sessions,
+ count(DISTINCT ip_hash) FILTER (WHERE ip_hash IS NOT NULL) AS ip_hashes
+FROM visitor_events
+WHERE created_at >= now() - interval '30 days'
+GROUP BY day
+ORDER BY day DESC;
+```
+
+Top entry and content paths:
+
+```sql
+SELECT path, count(*) AS page_views, count(DISTINCT session_id) AS sessions
+FROM visitor_events
+WHERE created_at >= now() - interval '30 days'
+GROUP BY path
+ORDER BY page_views DESC
+LIMIT 30;
+```
+
+Referrer and campaign sources:
+
+```sql
+SELECT
+ coalesce(utm_source, '(direct)') AS utm_source,
+ coalesce(referrer, '(none)') AS referrer,
+ count(*) AS page_views,
+ count(DISTINCT session_id) AS sessions
+FROM visitor_events
+WHERE created_at >= now() - interval '30 days'
+GROUP BY utm_source, referrer
+ORDER BY page_views DESC
+LIMIT 30;
+```
+
+Repeat visitors by IP hash:
+
+```sql
+SELECT
+ ip_hash,
+ min(created_at) AS first_seen_at,
+ max(created_at) AS last_seen_at,
+ count(*) AS page_views,
+ count(DISTINCT session_id) AS sessions,
+ array_agg(DISTINCT path) FILTER (WHERE path IS NOT NULL) AS paths
+FROM visitor_events
+WHERE ip_hash IS NOT NULL
+GROUP BY ip_hash
+HAVING count(*) > 1
+ORDER BY last_seen_at DESC
+LIMIT 50;
+```
+
+Visitor timeline by session:
+
+```sql
+SELECT created_at, path, referrer, utm_source, utm_campaign, country, geo_region, geo_city
+FROM visitor_events
+WHERE session_id = ''
+ORDER BY created_at;
+```
+
+Question timeline with visitor context:
+
+```sql
+SELECT
+ created_at,
+ session_id,
+ ip_hash,
+ country,
+ geo_region,
+ geo_city,
+ page_path,
+ question_redacted,
+ answer_preview,
+ answer_mode
+FROM ask_events
+ORDER BY created_at DESC
+LIMIT 50;
+```
+
Total questions:
```sql
@@ -119,6 +228,15 @@ ORDER BY created_at DESC
LIMIT 50;
```
+Recent questions with answer previews:
+
+```sql
+SELECT created_at, language, question_redacted, answer_preview, normalized_intent, answer_mode, confidence
+FROM ask_events
+ORDER BY created_at DESC
+LIMIT 50;
+```
+
Questions with no source docs:
```sql
diff --git a/docs/home-server-deploy.md b/docs/home-server-deploy.md
index f6c2fa6..1d99f5b 100644
--- a/docs/home-server-deploy.md
+++ b/docs/home-server-deploy.md
@@ -144,10 +144,12 @@ This applies:
- `db/migrations/007_add_answer_cache_invalidation.sql`
- `db/migrations/008_create_rate_limit_buckets.sql`
- `db/migrations/009_create_ask_events.sql`
+- `db/migrations/010_add_ask_event_answer_preview.sql`
+- `db/migrations/011_add_visitor_events_and_identity_context.sql`
The script runs `psql` inside the `postgres` Compose service and reads `POSTGRES_USER` and `POSTGRES_DB` from `.env.production`.
-Migration `003` is required for `answer_cache`, `ai_provider_usage`, and `ai_provider_status`. Migration `004` is required for language-specific RAG search and sync locking. Migration `005` is required for short-lived RAG search result caching. Migration `006` adds provider usage metadata. Migration `007` adds entity/source-chunk indexes plus soft invalidation for `answer_cache`, and lets RAG sync record deleted chunks. Migration `008` adds persistent `rate_limit_buckets` for Postgres-backed chat and feedback request limits. Migration `009` adds privacy-conscious `ask_events` logging for sanitized AskOosu question analytics. If production was already deployed before these migrations existed, run `scripts/prod-migrate.sh` again after pulling the latest code.
+Migration `003` is required for `answer_cache`, `ai_provider_usage`, and `ai_provider_status`. Migration `004` is required for language-specific RAG search and sync locking. Migration `005` is required for short-lived RAG search result caching. Migration `006` adds provider usage metadata. Migration `007` adds entity/source-chunk indexes plus soft invalidation for `answer_cache`, and lets RAG sync record deleted chunks. Migration `008` adds persistent `rate_limit_buckets` for Postgres-backed chat and feedback request limits. Migration `009` adds privacy-conscious `ask_events` logging for sanitized AskOosu question analytics. Migration `010` adds a short sanitized `answer_preview` excerpt for answer-quality review. Migration `011` adds visitor page-view logging plus `ip_hash` and Cloudflare location context for visitor and question analytics. If production was already deployed before these migrations existed, run `scripts/prod-migrate.sh` again after pulling the latest code.
After a successful `/api/rag/sync`, AskOosu invalidates generated `answer_cache` rows by changed `matched_entity_ids` first. If changed chunks do not have entity ids, sync falls back to `source_chunk_ids` when possible and otherwise leaves stale rows to expire via `ASKOOSU_ANSWER_CACHE_TTL_HOURS`.
diff --git a/next.config.ts b/next.config.ts
index 02d29dd..37491a9 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -4,6 +4,47 @@ const nextConfig = {
images: {
domains: ['images.unsplash.com', 'assets.aceternity.com'],
},
+ async headers() {
+ return [
+ {
+ source:
+ '/:path(favicon.ico|favicon.svg|apple-touch-icon.png|llms.txt|llms-full.txt|ai-profile.md|ai-projects.md|ai-faq.md)',
+ headers: [
+ {
+ key: 'Cache-Control',
+ value: 'public, max-age=14400, s-maxage=86400, must-revalidate',
+ },
+ ],
+ },
+ {
+ source: '/images/:path*',
+ headers: [
+ {
+ key: 'Cache-Control',
+ value: 'public, max-age=86400, s-maxage=604800, must-revalidate',
+ },
+ ],
+ },
+ {
+ source: '/oosu-projects/:path*',
+ headers: [
+ {
+ key: 'Cache-Control',
+ value: 'public, max-age=86400, s-maxage=604800, must-revalidate',
+ },
+ ],
+ },
+ {
+ source: '/oosu-avatar/:path*',
+ headers: [
+ {
+ key: 'Cache-Control',
+ value: 'public, max-age=86400, s-maxage=604800, must-revalidate',
+ },
+ ],
+ },
+ ];
+ },
eslint: {
// Ne bloque PAS le build en cas d'erreurs eslint
ignoreDuringBuilds: true,
diff --git a/package.json b/package.json
index ad1d57f..5a1d79e 100644
--- a/package.json
+++ b/package.json
@@ -41,18 +41,23 @@
"ai": "^6.0.174",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "feed": "^5.2.1",
"framer-motion": "^12.5.0",
+ "gray-matter": "^4.0.3",
"lottie-web": "^5.13.0",
"lucide-react": "^0.483.0",
"motion": "^12.23.0",
"next": "15.1.9",
+ "next-mdx-remote": "^6.0.0",
"next-themes": "^0.4.6",
"pg": "^8.20.0",
"react": "^19.0.1",
"react-dom": "^19.0.1",
"react-github-btn": "^1.4.0",
"react-markdown": "^10.1.0",
+ "rehype-pretty-code": "^0.14.3",
"remark-gfm": "^4.0.1",
+ "shiki": "^4.0.2",
"sonner": "^2.0.1",
"tailwind-merge": "^3.0.2",
"tw-animate-css": "^1.2.4",
@@ -71,6 +76,7 @@
"eslint-config-next": "15.2.3",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-prettier": "^5.2.3",
+ "eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-tailwindcss": "^3.18.0",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index fa9b848..ca3c3d3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -64,7 +64,7 @@ importers:
version: 3.8.1
'@vercel/analytics':
specifier: ^1.5.0
- version: 1.5.0(next@15.1.9(@opentelemetry/api@1.9.0)(react-dom@19.0.1(react@19.0.1))(react@19.0.1))(react@19.0.1)
+ version: 1.5.0(next@15.1.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.1(react@19.0.1))(react@19.0.1))(react@19.0.1)
ai:
specifier: ^6.0.174
version: 6.0.174(zod@3.25.76)
@@ -74,9 +74,15 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
+ feed:
+ specifier: ^5.2.1
+ version: 5.2.1
framer-motion:
specifier: ^12.5.0
version: 12.5.0(react-dom@19.0.1(react@19.0.1))(react@19.0.1)
+ gray-matter:
+ specifier: ^4.0.3
+ version: 4.0.3
lottie-web:
specifier: ^5.13.0
version: 5.13.0
@@ -88,7 +94,10 @@ importers:
version: 12.23.0(react-dom@19.0.1(react@19.0.1))(react@19.0.1)
next:
specifier: 15.1.9
- version: 15.1.9(@opentelemetry/api@1.9.0)(react-dom@19.0.1(react@19.0.1))(react@19.0.1)
+ version: 15.1.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.1(react@19.0.1))(react@19.0.1)
+ next-mdx-remote:
+ specifier: ^6.0.0
+ version: 6.0.0(@types/react@19.0.12)(react@19.0.1)
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.0.1(react@19.0.1))(react@19.0.1)
@@ -107,9 +116,15 @@ importers:
react-markdown:
specifier: ^10.1.0
version: 10.1.0(@types/react@19.0.12)(react@19.0.1)
+ rehype-pretty-code:
+ specifier: ^0.14.3
+ version: 0.14.3(shiki@4.0.2)
remark-gfm:
specifier: ^4.0.1
version: 4.0.1
+ shiki:
+ specifier: ^4.0.2
+ version: 4.0.2
sonner:
specifier: ^2.0.1
version: 2.0.1(react-dom@19.0.1(react@19.0.1))(react@19.0.1)
@@ -159,6 +174,9 @@ importers:
eslint-plugin-prettier:
specifier: ^5.2.3
version: 5.2.3(eslint-config-prettier@10.1.1(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2))(prettier@3.5.3)
+ eslint-plugin-react-hooks:
+ specifier: ^7.1.1
+ version: 7.1.1(eslint@9.22.0(jiti@2.4.2))
eslint-plugin-tailwindcss:
specifier: ^3.18.0
version: 3.18.0(tailwindcss@4.0.14)
@@ -245,6 +263,73 @@ packages:
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
+ '@babel/code-frame@7.29.0':
+ resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/compat-data@7.29.3':
+ resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/core@7.29.0':
+ resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/generator@7.29.1':
+ resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-compilation-targets@7.28.6':
+ resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-globals@7.28.0':
+ resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-imports@7.28.6':
+ resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-transforms@7.28.6':
+ resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-string-parser@7.27.1':
+ resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.28.5':
+ resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-option@7.27.1':
+ resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helpers@7.29.2':
+ resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.29.3':
+ resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/template@7.28.6':
+ resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/traverse@7.29.0':
+ resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@7.29.0':
+ resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
+ engines: {node: '>=6.9.0'}
+
'@emnapi/core@1.3.1':
resolution: {integrity: sha512-pVGjBIt1Y6gg3EJN8jTcfpP/+uuRksIo055oE/OBkDNcjZqVbfkWCksG1Jp4yZnj3iKWyWX8fdG/j6UDYPbFog==}
@@ -444,6 +529,22 @@ packages:
cpu: [x64]
os: [win32]
+ '@jridgewell/gen-mapping@0.3.13':
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
+ '@jridgewell/remapping@2.3.5':
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+
'@lordicon/helpers@1.10.1':
resolution: {integrity: sha512-GUOffYns36Y7j2c0oWWx0AJB80unvVnYrF1VCXJ7dAGuCyFhxuaR6NrzKgDPbY98Vn1q6b+7rRdfUEyXISiQgQ==}
@@ -462,6 +563,15 @@ packages:
react-native:
optional: true
+ '@mdx-js/mdx@3.1.1':
+ resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==}
+
+ '@mdx-js/react@3.1.1':
+ resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==}
+ peerDependencies:
+ '@types/react': '>=16'
+ react: '>=16'
+
'@napi-rs/wasm-runtime@0.2.7':
resolution: {integrity: sha512-5yximcFK5FNompXfJFoWanu5l8v1hNGqNHh9du1xETp9HWk/B/PzvchX55WYOPaIeNglG8++68AAiauBAtbnzw==}
@@ -857,6 +967,37 @@ packages:
'@rushstack/eslint-patch@1.11.0':
resolution: {integrity: sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==}
+ '@shikijs/core@4.0.2':
+ resolution: {integrity: sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==}
+ engines: {node: '>=20'}
+
+ '@shikijs/engine-javascript@4.0.2':
+ resolution: {integrity: sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==}
+ engines: {node: '>=20'}
+
+ '@shikijs/engine-oniguruma@4.0.2':
+ resolution: {integrity: sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==}
+ engines: {node: '>=20'}
+
+ '@shikijs/langs@4.0.2':
+ resolution: {integrity: sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==}
+ engines: {node: '>=20'}
+
+ '@shikijs/primitive@4.0.2':
+ resolution: {integrity: sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==}
+ engines: {node: '>=20'}
+
+ '@shikijs/themes@4.0.2':
+ resolution: {integrity: sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==}
+ engines: {node: '>=20'}
+
+ '@shikijs/types@4.0.2':
+ resolution: {integrity: sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==}
+ engines: {node: '>=20'}
+
+ '@shikijs/vscode-textmate@10.0.2':
+ resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
+
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
@@ -1093,6 +1234,9 @@ packages:
'@types/mdast@4.0.4':
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
+ '@types/mdx@2.0.13':
+ resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==}
+
'@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
@@ -1282,6 +1426,9 @@ packages:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
+ argparse@1.0.10:
+ resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
+
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
@@ -1328,6 +1475,10 @@ packages:
ast-types-flow@0.0.8:
resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==}
+ astring@1.9.0:
+ resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==}
+ hasBin: true
+
async-function@1.0.0:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'}
@@ -1353,6 +1504,11 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+ baseline-browser-mapping@2.10.29:
+ resolution: {integrity: sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
bignumber.js@9.3.1:
resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
@@ -1366,6 +1522,11 @@ packages:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
+ browserslist@4.28.2:
+ resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
buffer-equal-constant-time@1.0.1:
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
@@ -1392,6 +1553,9 @@ packages:
caniuse-lite@1.0.30001706:
resolution: {integrity: sha512-3ZczoTApMAZwPKYWmwVbQMFpXBDds3/0VciVoUwPUbldlYyVLmRVuRs/PcUZtHpbLRpzzDvrvnFuREsGt6lUug==}
+ caniuse-lite@1.0.30001792:
+ resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==}
+
ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
@@ -1421,6 +1585,9 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
+ collapse-white-space@2.1.0:
+ resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==}
+
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -1441,6 +1608,9 @@ packages:
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@@ -1523,6 +1693,9 @@ packages:
ecdsa-sig-formatter@1.0.11:
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
+ electron-to-chromium@1.5.354:
+ resolution: {integrity: sha512-JaBHwWcfIdmSAfWM5l3uwjGd431j8YEMikZ+K/2nXVuBqJKyZ0f+2h4n4JY5AyNiZmnY9qQr2RU3v9DxDmHMNg==}
+
emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
@@ -1530,6 +1703,10 @@ packages:
resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
engines: {node: '>=10.13.0'}
+ entities@6.0.1:
+ resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
+ engines: {node: '>=0.12'}
+
es-abstract@1.23.9:
resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==}
engines: {node: '>= 0.4'}
@@ -1562,6 +1739,16 @@ packages:
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
engines: {node: '>= 0.4'}
+ esast-util-from-estree@2.0.0:
+ resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==}
+
+ esast-util-from-js@2.0.1:
+ resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==}
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
@@ -1658,6 +1845,12 @@ packages:
peerDependencies:
eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
+ eslint-plugin-react-hooks@7.1.1:
+ resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0
+
eslint-plugin-react@7.37.4:
resolution: {integrity: sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==}
engines: {node: '>=4'}
@@ -1696,6 +1889,11 @@ packages:
resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ esprima@4.0.1:
+ resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
+ engines: {node: '>=4'}
+ hasBin: true
+
esquery@1.6.0:
resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
engines: {node: '>=0.10'}
@@ -1708,9 +1906,27 @@ packages:
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
engines: {node: '>=4.0'}
+ estree-util-attach-comments@3.0.0:
+ resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==}
+
+ estree-util-build-jsx@3.0.1:
+ resolution: {integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==}
+
estree-util-is-identifier-name@3.0.0:
resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==}
+ estree-util-scope@1.0.0:
+ resolution: {integrity: sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==}
+
+ estree-util-to-js@2.0.0:
+ resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==}
+
+ estree-util-visit@2.0.0:
+ resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==}
+
+ estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
@@ -1719,6 +1935,10 @@ packages:
resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==}
engines: {node: '>=18.0.0'}
+ extend-shallow@2.0.1:
+ resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
+ engines: {node: '>=0.10.0'}
+
extend@3.0.2:
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
@@ -1753,6 +1973,10 @@ packages:
picomatch:
optional: true
+ feed@5.2.1:
+ resolution: {integrity: sha512-jTynzYPWs9ALjro0GW8j7sv9y7cJBeOdD4Y88kVqYy/eyusIX3g+499JiTDIlD9Ge/unebx57T4Uzo6vpYvMtA==}
+ engines: {node: '>=20', pnpm: '>=10'}
+
fetch-blob@3.2.0:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
@@ -1830,6 +2054,10 @@ packages:
resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==}
engines: {node: '>=18'}
+ gensync@1.0.0-beta.2:
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
+ engines: {node: '>=6.9.0'}
+
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
@@ -1886,6 +2114,10 @@ packages:
graphemer@1.4.0:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
+ gray-matter@4.0.3:
+ resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==}
+ engines: {node: '>=6.0'}
+
has-bigints@1.1.0:
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
engines: {node: '>= 0.4'}
@@ -1913,15 +2145,45 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
+ hast-util-from-html@2.0.3:
+ resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==}
+
+ hast-util-from-parse5@8.0.3:
+ resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==}
+
+ hast-util-parse-selector@4.0.0:
+ resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
+
+ hast-util-to-estree@3.1.3:
+ resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==}
+
+ hast-util-to-html@9.0.5:
+ resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==}
+
hast-util-to-jsx-runtime@2.3.6:
resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==}
+ hast-util-to-string@3.0.1:
+ resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==}
+
hast-util-whitespace@3.0.0:
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
+ hastscript@9.0.1:
+ resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
+
+ hermes-estree@0.25.1:
+ resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
+
+ hermes-parser@0.25.1:
+ resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
+
html-url-attributes@3.0.1:
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
+ html-void-elements@3.0.0:
+ resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
+
https-proxy-agent@7.0.6:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
@@ -1992,6 +2254,10 @@ packages:
is-decimal@2.0.1:
resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
+ is-extendable@0.1.1:
+ resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==}
+ engines: {node: '>=0.10.0'}
+
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
@@ -2080,10 +2346,19 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+ js-yaml@3.14.2:
+ resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==}
+ hasBin: true
+
js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
json-bigint@1.0.0:
resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==}
@@ -2103,6 +2378,11 @@ packages:
resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
hasBin: true
+ json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
jsx-ast-utils@3.3.5:
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
engines: {node: '>=4.0'}
@@ -2116,6 +2396,10 @@ packages:
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+ kind-of@6.0.3:
+ resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
+ engines: {node: '>=0.10.0'}
+
language-subtag-registry@0.3.23:
resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==}
@@ -2212,11 +2496,18 @@ packages:
lottie-web@5.13.0:
resolution: {integrity: sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ==}
+ lru-cache@5.1.1:
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+
lucide-react@0.483.0:
resolution: {integrity: sha512-WldsY17Qb/T3VZdMnVQ9C3DDIP7h1ViDTHVdVGnLZcvHNg30zH/MTQ04RTORjexoGmpsXroiQXZ4QyR0kBy0FA==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ markdown-extensions@2.0.0:
+ resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==}
+ engines: {node: '>=16'}
+
markdown-table@3.0.4:
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
@@ -2254,6 +2545,9 @@ packages:
mdast-util-mdx-jsx@3.2.0:
resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==}
+ mdast-util-mdx@3.0.0:
+ resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==}
+
mdast-util-mdxjs-esm@2.0.1:
resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==}
@@ -2297,12 +2591,30 @@ packages:
micromark-extension-gfm@3.0.0:
resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==}
+ micromark-extension-mdx-expression@3.0.1:
+ resolution: {integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==}
+
+ micromark-extension-mdx-jsx@3.0.2:
+ resolution: {integrity: sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==}
+
+ micromark-extension-mdx-md@2.0.0:
+ resolution: {integrity: sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==}
+
+ micromark-extension-mdxjs-esm@3.0.0:
+ resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==}
+
+ micromark-extension-mdxjs@3.0.0:
+ resolution: {integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==}
+
micromark-factory-destination@2.0.1:
resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==}
micromark-factory-label@2.0.1:
resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==}
+ micromark-factory-mdx-expression@2.0.3:
+ resolution: {integrity: sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==}
+
micromark-factory-space@2.0.1:
resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==}
@@ -2333,6 +2645,9 @@ packages:
micromark-util-encode@2.0.1:
resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==}
+ micromark-util-events-to-acorn@2.0.3:
+ resolution: {integrity: sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==}
+
micromark-util-html-tag-name@2.0.1:
resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==}
@@ -2408,6 +2723,12 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+ next-mdx-remote@6.0.0:
+ resolution: {integrity: sha512-cJEpEZlgD6xGjB4jL8BnI8FaYdN9BzZM4NwadPe1YQr7pqoWjg9EBCMv3nXBkuHqMRfv2y33SzUsuyNh9LFAQQ==}
+ engines: {node: '>=14', npm: '>=7'}
+ peerDependencies:
+ react: '>=16'
+
next-themes@0.4.6:
resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
peerDependencies:
@@ -2444,6 +2765,9 @@ packages:
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ node-releases@2.0.44:
+ resolution: {integrity: sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==}
+
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
@@ -2476,6 +2800,12 @@ packages:
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
engines: {node: '>= 0.4'}
+ oniguruma-parser@0.12.2:
+ resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==}
+
+ oniguruma-to-es@4.3.6:
+ resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==}
+
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@@ -2499,6 +2829,12 @@ packages:
parse-entities@4.0.2:
resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
+ parse-numeric-range@1.3.0:
+ resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==}
+
+ parse5@7.3.0:
+ resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
+
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
@@ -2717,17 +3053,55 @@ packages:
resolution: {integrity: sha512-nVRaZCuEyvu69sWrkdwjP6QY57C+lY+uMNNMyWUFJb9Z/JlaBOQus7mSMfGYsblv7R691u6SSJA/dX9IRnyyLQ==}
engines: {node: '>=0.10.0'}
+ recma-build-jsx@1.0.0:
+ resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==}
+
+ recma-jsx@1.0.1:
+ resolution: {integrity: sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ recma-parse@1.0.0:
+ resolution: {integrity: sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==}
+
+ recma-stringify@1.0.0:
+ resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==}
+
reflect.getprototypeof@1.0.10:
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
engines: {node: '>= 0.4'}
+ regex-recursion@6.0.2:
+ resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==}
+
+ regex-utilities@2.3.0:
+ resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==}
+
+ regex@6.1.0:
+ resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==}
+
regexp.prototype.flags@1.5.4:
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
engines: {node: '>= 0.4'}
+ rehype-parse@9.0.1:
+ resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==}
+
+ rehype-pretty-code@0.14.3:
+ resolution: {integrity: sha512-Cz692FeYusTjT5cfFWLc4r7JhgC3/JlJptgUh4iffBxWxUnWW1oqbWFi7jGCeq00DYUm8yzoTnvpocaYa5x82g==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ shiki: ^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0
+
+ rehype-recma@1.0.0:
+ resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==}
+
remark-gfm@4.0.1:
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
+ remark-mdx@3.1.1:
+ resolution: {integrity: sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==}
+
remark-parse@11.0.0:
resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==}
@@ -2778,9 +3152,17 @@ packages:
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
engines: {node: '>= 0.4'}
+ sax@1.6.0:
+ resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==}
+ engines: {node: '>=11.0.0'}
+
scheduler@0.25.0:
resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==}
+ section-matter@1.0.0:
+ resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==}
+ engines: {node: '>=4'}
+
semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
@@ -2814,6 +3196,10 @@ packages:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
+ shiki@4.0.2:
+ resolution: {integrity: sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==}
+ engines: {node: '>=20'}
+
side-channel-list@1.0.0:
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
engines: {node: '>= 0.4'}
@@ -2843,6 +3229,10 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
+ source-map@0.7.6:
+ resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
+ engines: {node: '>= 12'}
+
space-separated-tokens@2.0.2:
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
@@ -2850,6 +3240,9 @@ packages:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
+ sprintf-js@1.0.3:
+ resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
+
stable-hash@0.0.5:
resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
@@ -2883,6 +3276,10 @@ packages:
stringify-entities@4.0.4:
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
+ strip-bom-string@1.0.0:
+ resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==}
+ engines: {node: '>=0.10.0'}
+
strip-bom@3.0.0:
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
engines: {node: '>=4'}
@@ -3008,9 +3405,15 @@ packages:
unist-util-is@6.0.0:
resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==}
+ unist-util-position-from-estree@2.0.0:
+ resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==}
+
unist-util-position@5.0.0:
resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==}
+ unist-util-remove@4.0.0:
+ resolution: {integrity: sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg==}
+
unist-util-stringify-position@4.0.0:
resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
@@ -3020,6 +3423,15 @@ packages:
unist-util-visit@5.0.0:
resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==}
+ unist-util-visit@5.1.0:
+ resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==}
+
+ update-browserslist-db@1.2.3:
+ resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
@@ -3054,12 +3466,21 @@ packages:
react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
+ vfile-location@5.0.3:
+ resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
+
+ vfile-matter@5.0.1:
+ resolution: {integrity: sha512-o6roP82AiX0XfkyTHyRCMXgHfltUNlXSEqCIS80f+mbAyiQBE2fxtDVMtseyytGx75sihiJFo/zR6r/4LTs2Cw==}
+
vfile-message@4.0.2:
resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==}
vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
+ web-namespaces@2.0.1:
+ resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
+
web-streams-polyfill@3.3.3:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
@@ -3089,14 +3510,32 @@ packages:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
+ xml-js@1.6.11:
+ resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
+ hasBin: true
+
xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
+ yallist@3.1.1:
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+
+ yaml@2.9.0:
+ resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==}
+ engines: {node: '>= 14.6'}
+ hasBin: true
+
yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
+ zod-validation-error@4.0.2:
+ resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ zod: ^3.25.0 || ^4.0.0
+
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
@@ -3184,6 +3623,106 @@ snapshots:
'@alloc/quick-lru@5.2.0': {}
+ '@babel/code-frame@7.29.0':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.28.5
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/compat-data@7.29.3': {}
+
+ '@babel/core@7.29.0':
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/generator': 7.29.1
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
+ '@babel/helpers': 7.29.2
+ '@babel/parser': 7.29.3
+ '@babel/template': 7.28.6
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ '@jridgewell/remapping': 2.3.5
+ convert-source-map: 2.0.0
+ debug: 4.4.0
+ gensync: 1.0.0-beta.2
+ json5: 2.2.3
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/generator@7.29.1':
+ dependencies:
+ '@babel/parser': 7.29.3
+ '@babel/types': 7.29.0
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+ jsesc: 3.1.0
+
+ '@babel/helper-compilation-targets@7.28.6':
+ dependencies:
+ '@babel/compat-data': 7.29.3
+ '@babel/helper-validator-option': 7.27.1
+ browserslist: 4.28.2
+ lru-cache: 5.1.1
+ semver: 6.3.1
+
+ '@babel/helper-globals@7.28.0': {}
+
+ '@babel/helper-module-imports@7.28.6':
+ dependencies:
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-module-imports': 7.28.6
+ '@babel/helper-validator-identifier': 7.28.5
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-string-parser@7.27.1': {}
+
+ '@babel/helper-validator-identifier@7.28.5': {}
+
+ '@babel/helper-validator-option@7.27.1': {}
+
+ '@babel/helpers@7.29.2':
+ dependencies:
+ '@babel/template': 7.28.6
+ '@babel/types': 7.29.0
+
+ '@babel/parser@7.29.3':
+ dependencies:
+ '@babel/types': 7.29.0
+
+ '@babel/template@7.28.6':
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/parser': 7.29.3
+ '@babel/types': 7.29.0
+
+ '@babel/traverse@7.29.0':
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/generator': 7.29.1
+ '@babel/helper-globals': 7.28.0
+ '@babel/parser': 7.29.3
+ '@babel/template': 7.28.6
+ '@babel/types': 7.29.0
+ debug: 4.4.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/types@7.29.0':
+ dependencies:
+ '@babel/helper-string-parser': 7.27.1
+ '@babel/helper-validator-identifier': 7.28.5
+
'@emnapi/core@1.3.1':
dependencies:
'@emnapi/wasi-threads': 1.0.1
@@ -3349,6 +3888,25 @@ snapshots:
'@img/sharp-win32-x64@0.33.5':
optional: true
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/remapping@2.3.5':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.5
+
'@lordicon/helpers@1.10.1': {}
'@lordicon/react@1.10.1(lottie-web@5.13.0)(react@19.0.1)':
@@ -3358,6 +3916,42 @@ snapshots:
optionalDependencies:
lottie-web: 5.13.0
+ '@mdx-js/mdx@3.1.1':
+ dependencies:
+ '@types/estree': 1.0.6
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.4
+ '@types/mdx': 2.0.13
+ acorn: 8.14.1
+ collapse-white-space: 2.1.0
+ devlop: 1.1.0
+ estree-util-is-identifier-name: 3.0.0
+ estree-util-scope: 1.0.0
+ estree-walker: 3.0.3
+ hast-util-to-jsx-runtime: 2.3.6
+ markdown-extensions: 2.0.0
+ recma-build-jsx: 1.0.0
+ recma-jsx: 1.0.1(acorn@8.14.1)
+ recma-stringify: 1.0.0
+ rehype-recma: 1.0.0
+ remark-mdx: 3.1.1
+ remark-parse: 11.0.0
+ remark-rehype: 11.1.1
+ source-map: 0.7.6
+ unified: 11.0.5
+ unist-util-position-from-estree: 2.0.0
+ unist-util-stringify-position: 4.0.0
+ unist-util-visit: 5.1.0
+ vfile: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@mdx-js/react@3.1.1(@types/react@19.0.12)(react@19.0.1)':
+ dependencies:
+ '@types/mdx': 2.0.13
+ '@types/react': 19.0.12
+ react: 19.0.1
+
'@napi-rs/wasm-runtime@0.2.7':
dependencies:
'@emnapi/core': 1.3.1
@@ -3690,6 +4284,46 @@ snapshots:
'@rushstack/eslint-patch@1.11.0': {}
+ '@shikijs/core@4.0.2':
+ dependencies:
+ '@shikijs/primitive': 4.0.2
+ '@shikijs/types': 4.0.2
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.4
+ hast-util-to-html: 9.0.5
+
+ '@shikijs/engine-javascript@4.0.2':
+ dependencies:
+ '@shikijs/types': 4.0.2
+ '@shikijs/vscode-textmate': 10.0.2
+ oniguruma-to-es: 4.3.6
+
+ '@shikijs/engine-oniguruma@4.0.2':
+ dependencies:
+ '@shikijs/types': 4.0.2
+ '@shikijs/vscode-textmate': 10.0.2
+
+ '@shikijs/langs@4.0.2':
+ dependencies:
+ '@shikijs/types': 4.0.2
+
+ '@shikijs/primitive@4.0.2':
+ dependencies:
+ '@shikijs/types': 4.0.2
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.4
+
+ '@shikijs/themes@4.0.2':
+ dependencies:
+ '@shikijs/types': 4.0.2
+
+ '@shikijs/types@4.0.2':
+ dependencies:
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.4
+
+ '@shikijs/vscode-textmate@10.0.2': {}
+
'@standard-schema/spec@1.1.0': {}
'@swc/counter@0.1.3': {}
@@ -3976,6 +4610,8 @@ snapshots:
dependencies:
'@types/unist': 3.0.3
+ '@types/mdx@2.0.13': {}
+
'@types/ms@2.1.0': {}
'@types/node@20.17.24':
@@ -4114,9 +4750,9 @@ snapshots:
'@unrs/rspack-resolver-binding-win32-x64-msvc@1.2.2':
optional: true
- '@vercel/analytics@1.5.0(next@15.1.9(@opentelemetry/api@1.9.0)(react-dom@19.0.1(react@19.0.1))(react@19.0.1))(react@19.0.1)':
+ '@vercel/analytics@1.5.0(next@15.1.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.1(react@19.0.1))(react@19.0.1))(react@19.0.1)':
optionalDependencies:
- next: 15.1.9(@opentelemetry/api@1.9.0)(react-dom@19.0.1(react@19.0.1))(react@19.0.1)
+ next: 15.1.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.1(react@19.0.1))(react@19.0.1)
react: 19.0.1
'@vercel/oidc@3.2.0': {}
@@ -4148,6 +4784,10 @@ snapshots:
dependencies:
color-convert: 2.0.1
+ argparse@1.0.10:
+ dependencies:
+ sprintf-js: 1.0.3
+
argparse@2.0.1: {}
aria-hidden@1.2.4:
@@ -4223,6 +4863,8 @@ snapshots:
ast-types-flow@0.0.8: {}
+ astring@1.9.0: {}
+
async-function@1.0.0: {}
available-typed-arrays@1.0.7:
@@ -4239,6 +4881,8 @@ snapshots:
base64-js@1.5.1: {}
+ baseline-browser-mapping@2.10.29: {}
+
bignumber.js@9.3.1: {}
brace-expansion@1.1.11:
@@ -4254,6 +4898,14 @@ snapshots:
dependencies:
fill-range: 7.1.1
+ browserslist@4.28.2:
+ dependencies:
+ baseline-browser-mapping: 2.10.29
+ caniuse-lite: 1.0.30001792
+ electron-to-chromium: 1.5.354
+ node-releases: 2.0.44
+ update-browserslist-db: 1.2.3(browserslist@4.28.2)
+
buffer-equal-constant-time@1.0.1: {}
busboy@1.6.0:
@@ -4281,6 +4933,8 @@ snapshots:
caniuse-lite@1.0.30001706: {}
+ caniuse-lite@1.0.30001792: {}
+
ccount@2.0.1: {}
chalk@4.1.2:
@@ -4304,6 +4958,8 @@ snapshots:
clsx@2.1.1: {}
+ collapse-white-space@2.1.0: {}
+
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -4326,6 +4982,8 @@ snapshots:
concat-map@0.0.1: {}
+ convert-source-map@2.0.0: {}
+
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@@ -4406,6 +5064,8 @@ snapshots:
dependencies:
safe-buffer: 5.2.1
+ electron-to-chromium@1.5.354: {}
+
emoji-regex@9.2.2: {}
enhanced-resolve@5.18.1:
@@ -4413,6 +5073,8 @@ snapshots:
graceful-fs: 4.2.11
tapable: 2.2.1
+ entities@6.0.1: {}
+
es-abstract@1.23.9:
dependencies:
array-buffer-byte-length: 1.0.2
@@ -4511,13 +5173,29 @@ snapshots:
is-date-object: 1.1.0
is-symbol: 1.1.1
- escape-string-regexp@4.0.0: {}
-
- escape-string-regexp@5.0.0: {}
-
- eslint-config-next@15.2.3(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2):
+ esast-util-from-estree@2.0.0:
dependencies:
- '@next/eslint-plugin-next': 15.2.3
+ '@types/estree-jsx': 1.0.5
+ devlop: 1.1.0
+ estree-util-visit: 2.0.0
+ unist-util-position-from-estree: 2.0.0
+
+ esast-util-from-js@2.0.1:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ acorn: 8.14.1
+ esast-util-from-estree: 2.0.0
+ vfile-message: 4.0.2
+
+ escalade@3.2.0: {}
+
+ escape-string-regexp@4.0.0: {}
+
+ escape-string-regexp@5.0.0: {}
+
+ eslint-config-next@15.2.3(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2):
+ dependencies:
+ '@next/eslint-plugin-next': 15.2.3
'@rushstack/eslint-patch': 1.11.0
'@typescript-eslint/eslint-plugin': 8.27.0(@typescript-eslint/parser@8.27.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)
'@typescript-eslint/parser': 8.27.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)
@@ -4634,6 +5312,17 @@ snapshots:
dependencies:
eslint: 9.22.0(jiti@2.4.2)
+ eslint-plugin-react-hooks@7.1.1(eslint@9.22.0(jiti@2.4.2)):
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/parser': 7.29.3
+ eslint: 9.22.0(jiti@2.4.2)
+ hermes-parser: 0.25.1
+ zod: 3.25.76
+ zod-validation-error: 4.0.2(zod@3.25.76)
+ transitivePeerDependencies:
+ - supports-color
+
eslint-plugin-react@7.37.4(eslint@9.22.0(jiti@2.4.2)):
dependencies:
array-includes: 3.1.8
@@ -4719,6 +5408,8 @@ snapshots:
acorn-jsx: 5.3.2(acorn@8.14.1)
eslint-visitor-keys: 4.2.0
+ esprima@4.0.1: {}
+
esquery@1.6.0:
dependencies:
estraverse: 5.3.0
@@ -4729,12 +5420,47 @@ snapshots:
estraverse@5.3.0: {}
+ estree-util-attach-comments@3.0.0:
+ dependencies:
+ '@types/estree': 1.0.6
+
+ estree-util-build-jsx@3.0.1:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ devlop: 1.1.0
+ estree-util-is-identifier-name: 3.0.0
+ estree-walker: 3.0.3
+
estree-util-is-identifier-name@3.0.0: {}
+ estree-util-scope@1.0.0:
+ dependencies:
+ '@types/estree': 1.0.6
+ devlop: 1.1.0
+
+ estree-util-to-js@2.0.0:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ astring: 1.9.0
+ source-map: 0.7.6
+
+ estree-util-visit@2.0.0:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ '@types/unist': 3.0.3
+
+ estree-walker@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.6
+
esutils@2.0.3: {}
eventsource-parser@3.0.8: {}
+ extend-shallow@2.0.1:
+ dependencies:
+ is-extendable: 0.1.1
+
extend@3.0.2: {}
fast-deep-equal@3.1.3: {}
@@ -4769,6 +5495,10 @@ snapshots:
optionalDependencies:
picomatch: 4.0.2
+ feed@5.2.1:
+ dependencies:
+ xml-js: 1.6.11
+
fetch-blob@3.2.0:
dependencies:
node-domexception: 1.0.0
@@ -4849,6 +5579,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ gensync@1.0.0-beta.2: {}
+
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -4915,6 +5647,13 @@ snapshots:
graphemer@1.4.0: {}
+ gray-matter@4.0.3:
+ dependencies:
+ js-yaml: 3.14.2
+ kind-of: 6.0.3
+ section-matter: 1.0.0
+ strip-bom-string: 1.0.0
+
has-bigints@1.1.0: {}
has-flag@4.0.0: {}
@@ -4937,6 +5676,65 @@ snapshots:
dependencies:
function-bind: 1.1.2
+ hast-util-from-html@2.0.3:
+ dependencies:
+ '@types/hast': 3.0.4
+ devlop: 1.1.0
+ hast-util-from-parse5: 8.0.3
+ parse5: 7.3.0
+ vfile: 6.0.3
+ vfile-message: 4.0.2
+
+ hast-util-from-parse5@8.0.3:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/unist': 3.0.3
+ devlop: 1.1.0
+ hastscript: 9.0.1
+ property-information: 7.0.0
+ vfile: 6.0.3
+ vfile-location: 5.0.3
+ web-namespaces: 2.0.1
+
+ hast-util-parse-selector@4.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+
+ hast-util-to-estree@3.1.3:
+ dependencies:
+ '@types/estree': 1.0.6
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.4
+ comma-separated-tokens: 2.0.3
+ devlop: 1.1.0
+ estree-util-attach-comments: 3.0.0
+ estree-util-is-identifier-name: 3.0.0
+ hast-util-whitespace: 3.0.0
+ mdast-util-mdx-expression: 2.0.1
+ mdast-util-mdx-jsx: 3.2.0
+ mdast-util-mdxjs-esm: 2.0.1
+ property-information: 7.0.0
+ space-separated-tokens: 2.0.2
+ style-to-js: 1.1.16
+ unist-util-position: 5.0.0
+ zwitch: 2.0.4
+ transitivePeerDependencies:
+ - supports-color
+
+ hast-util-to-html@9.0.5:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/unist': 3.0.3
+ ccount: 2.0.1
+ comma-separated-tokens: 2.0.3
+ hast-util-whitespace: 3.0.0
+ html-void-elements: 3.0.0
+ mdast-util-to-hast: 13.2.0
+ property-information: 7.0.0
+ space-separated-tokens: 2.0.2
+ stringify-entities: 4.0.4
+ zwitch: 2.0.4
+
hast-util-to-jsx-runtime@2.3.6:
dependencies:
'@types/estree': 1.0.6
@@ -4957,12 +5755,32 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ hast-util-to-string@3.0.1:
+ dependencies:
+ '@types/hast': 3.0.4
+
hast-util-whitespace@3.0.0:
dependencies:
'@types/hast': 3.0.4
+ hastscript@9.0.1:
+ dependencies:
+ '@types/hast': 3.0.4
+ comma-separated-tokens: 2.0.3
+ hast-util-parse-selector: 4.0.0
+ property-information: 7.0.0
+ space-separated-tokens: 2.0.2
+
+ hermes-estree@0.25.1: {}
+
+ hermes-parser@0.25.1:
+ dependencies:
+ hermes-estree: 0.25.1
+
html-url-attributes@3.0.1: {}
+ html-void-elements@3.0.0: {}
+
https-proxy-agent@7.0.6:
dependencies:
agent-base: 7.1.4
@@ -5043,6 +5861,8 @@ snapshots:
is-decimal@2.0.1: {}
+ is-extendable@0.1.1: {}
+
is-extglob@2.1.1: {}
is-finalizationregistry@1.1.1:
@@ -5129,10 +5949,17 @@ snapshots:
js-tokens@4.0.0: {}
+ js-yaml@3.14.2:
+ dependencies:
+ argparse: 1.0.10
+ esprima: 4.0.1
+
js-yaml@4.1.0:
dependencies:
argparse: 2.0.1
+ jsesc@3.1.0: {}
+
json-bigint@1.0.0:
dependencies:
bignumber.js: 9.3.1
@@ -5149,6 +5976,8 @@ snapshots:
dependencies:
minimist: 1.2.8
+ json5@2.2.3: {}
+
jsx-ast-utils@3.3.5:
dependencies:
array-includes: 3.1.8
@@ -5171,6 +6000,8 @@ snapshots:
dependencies:
json-buffer: 3.0.1
+ kind-of@6.0.3: {}
+
language-subtag-registry@0.3.23: {}
language-tags@1.0.9:
@@ -5241,10 +6072,16 @@ snapshots:
lottie-web@5.13.0: {}
+ lru-cache@5.1.1:
+ dependencies:
+ yallist: 3.1.1
+
lucide-react@0.483.0(react@19.0.1):
dependencies:
react: 19.0.1
+ markdown-extensions@2.0.0: {}
+
markdown-table@3.0.4: {}
math-intrinsics@1.1.0: {}
@@ -5358,6 +6195,16 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ mdast-util-mdx@3.0.0:
+ dependencies:
+ mdast-util-from-markdown: 2.0.2
+ mdast-util-mdx-expression: 2.0.1
+ mdast-util-mdx-jsx: 3.2.0
+ mdast-util-mdxjs-esm: 2.0.1
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
mdast-util-mdxjs-esm@2.0.1:
dependencies:
'@types/estree-jsx': 1.0.5
@@ -5481,6 +6328,57 @@ snapshots:
micromark-util-combine-extensions: 2.0.1
micromark-util-types: 2.0.2
+ micromark-extension-mdx-expression@3.0.1:
+ dependencies:
+ '@types/estree': 1.0.6
+ devlop: 1.1.0
+ micromark-factory-mdx-expression: 2.0.3
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-events-to-acorn: 2.0.3
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-mdx-jsx@3.0.2:
+ dependencies:
+ '@types/estree': 1.0.6
+ devlop: 1.1.0
+ estree-util-is-identifier-name: 3.0.0
+ micromark-factory-mdx-expression: 2.0.3
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-events-to-acorn: 2.0.3
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ vfile-message: 4.0.2
+
+ micromark-extension-mdx-md@2.0.0:
+ dependencies:
+ micromark-util-types: 2.0.2
+
+ micromark-extension-mdxjs-esm@3.0.0:
+ dependencies:
+ '@types/estree': 1.0.6
+ devlop: 1.1.0
+ micromark-core-commonmark: 2.0.3
+ micromark-util-character: 2.1.1
+ micromark-util-events-to-acorn: 2.0.3
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ unist-util-position-from-estree: 2.0.0
+ vfile-message: 4.0.2
+
+ micromark-extension-mdxjs@3.0.0:
+ dependencies:
+ acorn: 8.14.1
+ acorn-jsx: 5.3.2(acorn@8.14.1)
+ micromark-extension-mdx-expression: 3.0.1
+ micromark-extension-mdx-jsx: 3.0.2
+ micromark-extension-mdx-md: 2.0.0
+ micromark-extension-mdxjs-esm: 3.0.0
+ micromark-util-combine-extensions: 2.0.1
+ micromark-util-types: 2.0.2
+
micromark-factory-destination@2.0.1:
dependencies:
micromark-util-character: 2.1.1
@@ -5494,6 +6392,18 @@ snapshots:
micromark-util-symbol: 2.0.1
micromark-util-types: 2.0.2
+ micromark-factory-mdx-expression@2.0.3:
+ dependencies:
+ '@types/estree': 1.0.6
+ devlop: 1.1.0
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-events-to-acorn: 2.0.3
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ unist-util-position-from-estree: 2.0.0
+ vfile-message: 4.0.2
+
micromark-factory-space@2.0.1:
dependencies:
micromark-util-character: 2.1.1
@@ -5546,6 +6456,16 @@ snapshots:
micromark-util-encode@2.0.1: {}
+ micromark-util-events-to-acorn@2.0.3:
+ dependencies:
+ '@types/estree': 1.0.6
+ '@types/unist': 3.0.3
+ devlop: 1.1.0
+ estree-util-visit: 2.0.0
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ vfile-message: 4.0.2
+
micromark-util-html-tag-name@2.0.1: {}
micromark-util-normalize-identifier@2.0.1:
@@ -5636,12 +6556,26 @@ snapshots:
natural-compare@1.4.0: {}
+ next-mdx-remote@6.0.0(@types/react@19.0.12)(react@19.0.1):
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@mdx-js/mdx': 3.1.1
+ '@mdx-js/react': 3.1.1(@types/react@19.0.12)(react@19.0.1)
+ react: 19.0.1
+ unist-util-remove: 4.0.0
+ unist-util-visit: 5.1.0
+ vfile: 6.0.3
+ vfile-matter: 5.0.1
+ transitivePeerDependencies:
+ - '@types/react'
+ - supports-color
+
next-themes@0.4.6(react-dom@19.0.1(react@19.0.1))(react@19.0.1):
dependencies:
react: 19.0.1
react-dom: 19.0.1(react@19.0.1)
- next@15.1.9(@opentelemetry/api@1.9.0)(react-dom@19.0.1(react@19.0.1))(react@19.0.1):
+ next@15.1.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.1(react@19.0.1))(react@19.0.1):
dependencies:
'@next/env': 15.1.9
'@swc/counter': 0.1.3
@@ -5651,7 +6585,7 @@ snapshots:
postcss: 8.4.31
react: 19.0.1
react-dom: 19.0.1(react@19.0.1)
- styled-jsx: 5.1.6(react@19.0.1)
+ styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.0.1)
optionalDependencies:
'@next/swc-darwin-arm64': 15.1.9
'@next/swc-darwin-x64': 15.1.9
@@ -5675,6 +6609,8 @@ snapshots:
fetch-blob: 3.2.0
formdata-polyfill: 4.0.10
+ node-releases@2.0.44: {}
+
object-assign@4.1.1: {}
object-inspect@1.13.4: {}
@@ -5717,6 +6653,14 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.1.1
+ oniguruma-parser@0.12.2: {}
+
+ oniguruma-to-es@4.3.6:
+ dependencies:
+ oniguruma-parser: 0.12.2
+ regex: 6.1.0
+ regex-recursion: 6.0.2
+
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@@ -5754,6 +6698,12 @@ snapshots:
is-decimal: 2.0.1
is-hexadecimal: 2.0.1
+ parse-numeric-range@1.3.0: {}
+
+ parse5@7.3.0:
+ dependencies:
+ entities: 6.0.1
+
path-exists@4.0.0: {}
path-key@3.1.1: {}
@@ -5908,6 +6858,35 @@ snapshots:
react@19.0.1: {}
+ recma-build-jsx@1.0.0:
+ dependencies:
+ '@types/estree': 1.0.6
+ estree-util-build-jsx: 3.0.1
+ vfile: 6.0.3
+
+ recma-jsx@1.0.1(acorn@8.14.1):
+ dependencies:
+ acorn: 8.14.1
+ acorn-jsx: 5.3.2(acorn@8.14.1)
+ estree-util-to-js: 2.0.0
+ recma-parse: 1.0.0
+ recma-stringify: 1.0.0
+ unified: 11.0.5
+
+ recma-parse@1.0.0:
+ dependencies:
+ '@types/estree': 1.0.6
+ esast-util-from-js: 2.0.1
+ unified: 11.0.5
+ vfile: 6.0.3
+
+ recma-stringify@1.0.0:
+ dependencies:
+ '@types/estree': 1.0.6
+ estree-util-to-js: 2.0.0
+ unified: 11.0.5
+ vfile: 6.0.3
+
reflect.getprototypeof@1.0.10:
dependencies:
call-bind: 1.0.8
@@ -5919,6 +6898,16 @@ snapshots:
get-proto: 1.0.1
which-builtin-type: 1.2.1
+ regex-recursion@6.0.2:
+ dependencies:
+ regex-utilities: 2.3.0
+
+ regex-utilities@2.3.0: {}
+
+ regex@6.1.0:
+ dependencies:
+ regex-utilities: 2.3.0
+
regexp.prototype.flags@1.5.4:
dependencies:
call-bind: 1.0.8
@@ -5928,6 +6917,30 @@ snapshots:
gopd: 1.2.0
set-function-name: 2.0.2
+ rehype-parse@9.0.1:
+ dependencies:
+ '@types/hast': 3.0.4
+ hast-util-from-html: 2.0.3
+ unified: 11.0.5
+
+ rehype-pretty-code@0.14.3(shiki@4.0.2):
+ dependencies:
+ '@types/hast': 3.0.4
+ hast-util-to-string: 3.0.1
+ parse-numeric-range: 1.3.0
+ rehype-parse: 9.0.1
+ shiki: 4.0.2
+ unified: 11.0.5
+ unist-util-visit: 5.0.0
+
+ rehype-recma@1.0.0:
+ dependencies:
+ '@types/estree': 1.0.6
+ '@types/hast': 3.0.4
+ hast-util-to-estree: 3.1.3
+ transitivePeerDependencies:
+ - supports-color
+
remark-gfm@4.0.1:
dependencies:
'@types/mdast': 4.0.4
@@ -5939,6 +6952,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ remark-mdx@3.1.1:
+ dependencies:
+ mdast-util-mdx: 3.0.0
+ micromark-extension-mdxjs: 3.0.0
+ transitivePeerDependencies:
+ - supports-color
+
remark-parse@11.0.0:
dependencies:
'@types/mdast': 4.0.4
@@ -6019,8 +7039,15 @@ snapshots:
es-errors: 1.3.0
is-regex: 1.2.1
+ sax@1.6.0: {}
+
scheduler@0.25.0: {}
+ section-matter@1.0.0:
+ dependencies:
+ extend-shallow: 2.0.1
+ kind-of: 6.0.3
+
semver@6.3.1: {}
semver@7.7.1: {}
@@ -6080,6 +7107,17 @@ snapshots:
shebang-regex@3.0.0: {}
+ shiki@4.0.2:
+ dependencies:
+ '@shikijs/core': 4.0.2
+ '@shikijs/engine-javascript': 4.0.2
+ '@shikijs/engine-oniguruma': 4.0.2
+ '@shikijs/langs': 4.0.2
+ '@shikijs/themes': 4.0.2
+ '@shikijs/types': 4.0.2
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.4
+
side-channel-list@1.0.0:
dependencies:
es-errors: 1.3.0
@@ -6120,10 +7158,14 @@ snapshots:
source-map-js@1.2.1: {}
+ source-map@0.7.6: {}
+
space-separated-tokens@2.0.2: {}
split2@4.2.0: {}
+ sprintf-js@1.0.3: {}
+
stable-hash@0.0.5: {}
streamsearch@1.1.0: {}
@@ -6183,6 +7225,8 @@ snapshots:
character-entities-html4: 2.1.0
character-entities-legacy: 3.0.0
+ strip-bom-string@1.0.0: {}
+
strip-bom@3.0.0: {}
strip-json-comments@3.1.1: {}
@@ -6195,10 +7239,12 @@ snapshots:
dependencies:
inline-style-parser: 0.2.4
- styled-jsx@5.1.6(react@19.0.1):
+ styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.0.1):
dependencies:
client-only: 0.0.1
react: 19.0.1
+ optionalDependencies:
+ '@babel/core': 7.29.0
supports-color@7.2.0:
dependencies:
@@ -6315,10 +7361,20 @@ snapshots:
dependencies:
'@types/unist': 3.0.3
+ unist-util-position-from-estree@2.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+
unist-util-position@5.0.0:
dependencies:
'@types/unist': 3.0.3
+ unist-util-remove@4.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-is: 6.0.0
+ unist-util-visit-parents: 6.0.1
+
unist-util-stringify-position@4.0.0:
dependencies:
'@types/unist': 3.0.3
@@ -6334,6 +7390,18 @@ snapshots:
unist-util-is: 6.0.0
unist-util-visit-parents: 6.0.1
+ unist-util-visit@5.1.0:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-is: 6.0.0
+ unist-util-visit-parents: 6.0.1
+
+ update-browserslist-db@1.2.3(browserslist@4.28.2):
+ dependencies:
+ browserslist: 4.28.2
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
uri-js@4.4.1:
dependencies:
punycode: 2.3.1
@@ -6366,6 +7434,16 @@ snapshots:
- '@types/react'
- '@types/react-dom'
+ vfile-location@5.0.3:
+ dependencies:
+ '@types/unist': 3.0.3
+ vfile: 6.0.3
+
+ vfile-matter@5.0.1:
+ dependencies:
+ vfile: 6.0.3
+ yaml: 2.9.0
+
vfile-message@4.0.2:
dependencies:
'@types/unist': 3.0.3
@@ -6376,6 +7454,8 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.2
+ web-namespaces@2.0.1: {}
+
web-streams-polyfill@3.3.3: {}
which-boxed-primitive@1.1.1:
@@ -6425,10 +7505,22 @@ snapshots:
word-wrap@1.2.5: {}
+ xml-js@1.6.11:
+ dependencies:
+ sax: 1.6.0
+
xtend@4.0.2: {}
+ yallist@3.1.1: {}
+
+ yaml@2.9.0: {}
+
yocto-queue@0.1.0: {}
+ zod-validation-error@4.0.2(zod@3.25.76):
+ dependencies:
+ zod: 3.25.76
+
zod@3.25.76: {}
zwitch@2.0.4: {}
diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx
index 48f71a1..c57b3b6 100644
--- a/src/app/about/page.tsx
+++ b/src/app/about/page.tsx
@@ -1,52 +1,94 @@
import type { Metadata } from 'next';
import Link from 'next/link';
-import { PublicPageShell, TextSection } from '@/components/seo/public-page-shell';
-import { createPageMetadata } from '@/lib/seo';
+import {
+ PublicPageShell,
+ TextSection,
+} from '@/components/seo/public-page-shell';
+import { LocalizedText } from '@/components/localized-content';
+import { createPageMetadata, personJsonLd, siteUrl } from '@/lib/seo';
export const metadata: Metadata = createPageMetadata({
- title: 'About Oosu',
+ title: '장우수 소개 | Oosu · oosuhada 개발자',
description:
- 'Oosu is an AI-connected fullstack developer and product-minded builder focused on RAG portfolio systems, AI-assisted workflows, and trustworthy AI UX.',
+ '우수하다(oosuhada)라는 이름으로 활동하며 AI·RAG 서비스와 제품 중심 웹 애플리케이션을 만드는 AI·풀스택 개발자 장우수의 소개 페이지입니다.',
path: '/about',
keywords: ['About Oosu', 'oosuhada', 'AI-connected developer'],
});
+const profilePageJsonLd = {
+ '@context': 'https://schema.org',
+ '@type': 'ProfilePage',
+ '@id': `${siteUrl}/about#profile`,
+ url: `${siteUrl}/about`,
+ mainEntity: personJsonLd,
+};
+
export default function AboutPage() {
return (
-
-
-
- Oosu’s work focuses on connecting AI tools to real product contexts:
- planning, design, engineering, content, deployment, and operational
- feedback.
-
-
- The public portfolio avoids inflated claims. It presents a grounded
- profile: AI-connected fullstack developer, product-minded builder, and
- RAG portfolio system builder.
-
- Oosu does not use AI Director to claim that one person can replace an
- entire team. The phrase describes a working style: understanding the
- language of multiple product roles and using AI tools to connect ideas,
- implementation, content, deployment, and feedback.
+
- AI can expand speed and options, but product judgment still matters:
- what problem is worth solving, which interface feels trustworthy,
- which message sounds overclaimed, and which data should guide the next
- iteration.
+
-
+
- AskOosu connects a conversational portfolio UI, FAQ routing, Wiki/RAG
- source documents, source-aware answer rendering, and deployment
- operations. It is a practical example of AI-assisted product workflow,
- not just a chatbot prompt.
+
-
+
+ 관련 페이지:{' '}
+
+ AI 시대 개발자 경쟁력
+
+ ,{' '}
+
+ AI 경쟁력 FAQ
+
+ ,{' '}
+
+ AskOosu 프로젝트 노트
+
+ .
+
+
Related pages:{' '}
AI-era Developer Competitiveness
@@ -61,23 +88,38 @@ export default function AiDirectorPage() {
-
+
diff --git a/src/app/ai-era-developer/page.tsx b/src/app/ai-era-developer/page.tsx
index c7e5770..6949161 100644
--- a/src/app/ai-era-developer/page.tsx
+++ b/src/app/ai-era-developer/page.tsx
@@ -6,46 +6,76 @@ import {
TextSection,
} from '@/components/seo/public-page-shell';
import { createPageMetadata } from '@/lib/seo';
+import { LocalizedText } from '@/components/localized-content';
export const metadata: Metadata = createPageMetadata({
title: 'AI-era Developer Competitiveness',
description:
'Oosu’s AI-era competitiveness is based on connecting AI-generated options to real users, product judgment, UX quality, and responsible implementation.',
path: '/ai-era-developer',
- keywords: ['AI-era developer', 'trustworthy AI UX', 'AI application development'],
+ keywords: [
+ 'AI-era developer',
+ 'trustworthy AI UX',
+ 'AI application development',
+ ],
});
export default function AiEraDeveloperPage() {
return (
-
+
- AI can generate many options quickly. The developer’s job becomes more
- about choosing the right problem, checking whether the output fits the
- product context, and integrating it without losing quality or trust.
+
- Oosu’s public positioning is intentionally grounded: AI-connected
- fullstack developer, product-minded builder, RAG portfolio system
- builder, and trustworthy AI UX practitioner.
+
-
+
- This page does not claim that Oosu is a famous AI researcher, senior
- Silicon Valley engineer, or proven industry expert. It describes a
- portfolio direction and the work style shown by AskOosu and related
- projects.
+
-
-
+
+
+ 계속해서{' '}
+
+ AI Director형 제품 빌더
+
+ ,{' '}
+
+ AI 경쟁력 FAQ
+
+ , 또는{' '}
+
+ AskOosu
+
+ 를 확인해 보세요.
+
+
Continue with{' '}
AI Director-style Product Builder
@@ -62,23 +92,38 @@ export default function AiEraDeveloperPage() {
+ )}
+
+
+ );
+}
diff --git a/src/app/faq/ai-competitiveness/page.tsx b/src/app/faq/ai-competitiveness/page.tsx
index 159a80a..6b2a41a 100644
--- a/src/app/faq/ai-competitiveness/page.tsx
+++ b/src/app/faq/ai-competitiveness/page.tsx
@@ -17,50 +17,97 @@ export const metadata: Metadata = createPageMetadata({
const faqItems = [
{
- question: 'What can Oosu do that AI cannot replace?',
- answer:
- 'The safer answer is not that AI can never replace human work. Oosu focuses on judging AI output against real user problems, product context, UX quality, and trustworthy implementation.',
+ question: {
+ ko: 'AI가 대체하기 어려운 장우수의 역할은 무엇인가요?',
+ en: 'What can Oosu do that AI cannot replace?',
+ },
+ answer: {
+ ko: 'AI가 인간의 일을 절대 대체할 수 없다고 주장하기보다 AI 결과를 실제 사용자 문제, 제품 맥락, UX 품질, 신뢰할 수 있는 구현 기준으로 판단하는 데 집중합니다.',
+ en: 'The safer answer is not that AI can never replace human work. Oosu focuses on judging AI output against real user problems, product context, UX quality, and trustworthy implementation.',
+ },
},
{
- question: 'Does Oosu compete with AI?',
- answer:
- 'No. Oosu frames AI as an execution layer. His goal is to connect AI tools to real product contexts, not to compete with AI as if it were a person.',
+ question: {
+ ko: '장우수는 AI와 경쟁하나요?',
+ en: 'Does Oosu compete with AI?',
+ },
+ answer: {
+ ko: '아니요. AI를 실행 계층으로 보고 실제 제품 맥락에 연결합니다.',
+ en: 'No. Oosu frames AI as an execution layer. His goal is to connect AI tools to real product contexts, not to compete with AI as if it were a person.',
+ },
},
{
- question: 'How does Oosu use AI without becoming dependent on it?',
- answer:
- 'He defines the goal, scope, constraints, and validation criteria first, then reviews AI output against the actual codebase, UX flow, and security boundaries.',
+ question: {
+ ko: 'AI에 의존하지 않고 어떻게 활용하나요?',
+ en: 'How does Oosu use AI without becoming dependent on it?',
+ },
+ answer: {
+ ko: '목표, 범위, 제약, 검증 기준을 먼저 정의한 뒤 실제 코드베이스, UX 흐름, 보안 경계를 기준으로 AI 결과를 검토합니다.',
+ en: 'He defines the goal, scope, constraints, and validation criteria first, then reviews AI output against the actual codebase, UX flow, and security boundaries.',
+ },
},
{
- question: 'What does AI Director mean?',
- answer:
- 'AI Director is not a formal title. It is a working style for connecting planning, design, engineering, content, deployment, and feedback with AI as an execution partner.',
+ question: {
+ ko: 'AI Director는 무슨 뜻인가요?',
+ en: 'What does AI Director mean?',
+ },
+ answer: {
+ ko: '공식 직함이 아니라 AI를 실행 파트너로 삼아 기획, 디자인, 개발, 콘텐츠, 배포, 피드백을 연결하는 작업 방식입니다.',
+ en: 'AI Director is not a formal title. It is a working style for connecting planning, design, engineering, content, deployment, and feedback with AI as an execution partner.',
+ },
},
{
- question: 'How does Oosu review AI-generated code?',
- answer:
- 'He checks whether the output matches the existing file structure, uses the right API and UI fields, avoids secret exposure, and can be explained before it becomes product code.',
+ question: {
+ ko: 'AI 생성 코드를 어떻게 검토하나요?',
+ en: 'How does Oosu review AI-generated code?',
+ },
+ answer: {
+ ko: '기존 파일 구조와 맞는지, 올바른 API와 UI 필드를 사용하는지, 비밀정보를 노출하지 않는지, 제품 코드가 되기 전에 설명 가능한지를 확인합니다.',
+ en: 'He checks whether the output matches the existing file structure, uses the right API and UI fields, avoids secret exposure, and can be explained before it becomes product code.',
+ },
},
{
- question: 'Why does AskOosu use RAG and Wiki docs?',
- answer:
- 'AskOosu uses FAQ routing and Wiki/RAG documents so portfolio answers can stay grounded in source material instead of becoming generic chatbot responses.',
+ question: {
+ ko: 'AskOosu는 왜 RAG와 Wiki 문서를 사용하나요?',
+ en: 'Why does AskOosu use RAG and Wiki docs?',
+ },
+ answer: {
+ ko: '포트폴리오 답변이 일반적인 챗봇 응답이 되지 않고 원문 근거에 기반하도록 하기 위해서입니다.',
+ en: 'AskOosu uses FAQ routing and Wiki/RAG documents so portfolio answers can stay grounded in source material instead of becoming generic chatbot responses.',
+ },
},
];
export default function AiCompetitivenessFaqPage() {
return (
-
+
-
-
+
+
+
+ AI Director형 제품 빌더
+
+ ,{' '}
+
+ AI 시대 개발자 경쟁력
+
+ ,{' '}
+
+ AskOosu 프로젝트
+
+ 를 더 읽어보세요.
+
Oosu.dev publishes portfolio information, project summaries, public
contact links, and AI-readable summaries intended for search engines
and answer engines.
-
- Oosu.dev는 공개 포트폴리오 사이트입니다. 공개 프로젝트 설명, 공개
- 연락 링크, AI가 읽기 쉬운 요약 문서를 제공합니다.
+
+ Oosu.dev는 공개 포트폴리오 사이트입니다. 공개 프로젝트 설명, 공개 연락
+ 링크, AI가 읽기 쉬운 요약 문서를 제공합니다.
-
-
+
+
The site may use Cloudflare Web Analytics when the production
analytics token is configured. This is used to understand public page
traffic, referrers, approximate country-level traffic, and basic web
performance. Oosu.dev does not use this setup to store raw IP
addresses or precise geolocation in the application database.
-
- 운영 환경에서 Cloudflare Web Analytics 토큰이 설정된 경우, 공개
- 페이지 방문 흐름, referrer, 국가 단위의 대략적인 유입, 기본 성능
- 지표를 확인할 수 있습니다. 애플리케이션 DB에는 원본 IP 주소나
- 정밀 위치를 저장하지 않습니다.
+
+ 운영 환경에서 Cloudflare Web Analytics 토큰이 설정된 경우, 공개 페이지
+ 방문 흐름, referrer, 국가 단위의 대략적인 유입, 기본 성능 지표를
+ 확인할 수 있습니다. 애플리케이션 DB에는 원본 IP 주소나 정밀 위치를
+ 저장하지 않습니다.
-
-
+
+
AskOosu may store sanitized question events to improve portfolio UX,
answer quality, RAG/FAQ coverage, fallback detection, latency, and
feedback loops. Stored fields can include an anonymous session ID,
@@ -53,53 +63,61 @@ export default function PrivacyPage() {
value, page path, referrer without query string, UTM parameters,
country code, device type, browser, and operating system.
-
- AskOosu는 포트폴리오 UX 개선, 답변 품질 개선, RAG/FAQ 커버리지
- 보완, fallback 질문 탐지, 응답 속도 확인, 피드백 루프 개선을 위해
- 민감정보가 제거된 질문 이벤트를 저장할 수 있습니다. 저장 항목은
- 익명 세션 ID, 언어, redaction된 질문, intent, 답변 모드, confidence,
- source document ID, provider, latency, feedback 값, 페이지 경로,
- query string이 제거된 referrer, UTM, 국가 코드, 기기/브라우저/OS
- 정도로 제한됩니다.
+
+ AskOosu는 포트폴리오 UX 개선, 답변 품질 개선, RAG/FAQ 커버리지 보완,
+ fallback 질문 탐지, 응답 속도 확인, 피드백 루프 개선을 위해 민감정보가
+ 제거된 질문 이벤트를 저장할 수 있습니다. 저장 항목은 익명 세션 ID,
+ 언어, redaction된 질문, intent, 답변 모드, confidence, source document
+ ID, provider, latency, feedback 값, 페이지 경로, query string이 제거된
+ referrer, UTM, 국가 코드, 기기/브라우저/OS 정도로 제한됩니다.
-
-
+
+
Oosu.dev does not intentionally store raw IP addresses, precise
geolocation, passwords, API keys, bearer tokens, private keys, credit
card numbers, Korean resident registration number-like values, emails,
phone numbers, or secrets from user questions.
-
+
질문 저장 전에는 이메일, 전화번호, API key/token, bearer token,
private key, 긴 secret 형태의 문자열, 주민등록번호처럼 보이는 패턴,
카드번호처럼 보이는 패턴을 가능한 범위에서
[REDACTED_*] 형태로 치환합니다.
-
-
+
+
AskOosu can generate a random anonymous session ID in the browser and
store it in localStorage as askOosuSessionId. It is used
to understand repeated portfolio questions within the same browser. It
- is not tied to a login account, name, email address, or exact location.
+ is not tied to a login account, name, email address, or exact
+ location.
+
+
+ AskOosu는 브라우저에서 임의의 익명 세션 ID를 생성해 localStorage의{' '}
+ askOosuSessionId에 저장할 수 있습니다. 같은 브라우저의
+ 반복 질문 흐름을 이해하기 위한 값이며 로그인 계정, 이름, 이메일,
+ 정확한 위치와 연결되지 않습니다.
-
-
+
+
Sanitized AskOosu question logs are retained for product-quality
review unless they are manually deleted, aggregated, or no longer
needed for improving the portfolio experience. No automatic deletion
job is currently enabled.
-
- 민감정보가 제거된 질문 로그는 포트폴리오 UX와 답변 품질 개선을
- 위해 보관되며, 자동 삭제 작업은 현재 활성화되어 있지 않습니다. 필요
- 시 수동으로 삭제하거나 집계 데이터로 전환합니다.
+
+ 민감정보가 제거된 질문 로그는 포트폴리오 UX와 답변 품질 개선을 위해
+ 보관되며, 자동 삭제 작업은 현재 활성화되어 있지 않습니다. 필요 시
+ 수동으로 삭제하거나 집계 데이터로 전환합니다.
+ AskOosu를 통해 개인정보나 민감정보를 제출했다고 생각해 삭제를 원한다면
+ 대략적인 날짜와 질문 주제를 포함해{' '}
+
+ oosu.salon@gmail.com
+
+ 으로 연락해 주세요. 삭제 요청에는 비밀정보를 포함하지 마세요.
+
-
-
+
+
Public pages are indexable. API, admin, dashboard, private, and
preview-only paths are excluded from the sitemap and disallowed in the
robots policy.
+
+ 공개 페이지는 색인할 수 있습니다. API, 관리자, 대시보드, 비공개,
+ 미리보기 전용 경로는 sitemap에서 제외하고 robots 정책으로 차단합니다.
+
);
diff --git a/src/app/projects/askoosu/page.tsx b/src/app/projects/askoosu/page.tsx
index fb33a4c..025d9b9 100644
--- a/src/app/projects/askoosu/page.tsx
+++ b/src/app/projects/askoosu/page.tsx
@@ -5,6 +5,7 @@ import {
PublicPageShell,
TextSection,
} from '@/components/seo/public-page-shell';
+import { LocalizedText } from '@/components/localized-content';
import { oosuProjects } from '@/lib/oosu-profile';
import { createPageMetadata } from '@/lib/seo';
@@ -21,26 +22,53 @@ export const metadata: Metadata = createPageMetadata({
export default function AskOosuProjectPage() {
return (
-
-
{askoosu.description}
+
- AskOosu is designed as a product surface for trustworthy AI UX. It
- separates deterministic FAQ answers, RAG-backed evidence, and public
- source summaries so visitors can understand Oosu’s profile without
- relying on generic chatbot claims.
+
+
+
+
-
+
{askoosu.techStack.join(', ')}
-
-
+
+
+
+ AI 경쟁력 FAQ
+
+ ,{' '}
+
+ AI Director형 제품 빌더 노트
+
+ , 또는{' '}
+
+ AskOosu 채팅
+
+ 을 확인해 보세요.
+
+
Read the{' '}
AI competitiveness FAQ
@@ -57,18 +85,28 @@ export default function AskOosuProjectPage() {
-
+
diff --git a/src/app/projects/page.tsx b/src/app/projects/page.tsx
index 5d158cc..8d6bb3a 100644
--- a/src/app/projects/page.tsx
+++ b/src/app/projects/page.tsx
@@ -1,7 +1,11 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { ArrowUpRight } from 'lucide-react';
-import { PublicPageShell, TextSection } from '@/components/seo/public-page-shell';
+import { LocalizedText } from '@/components/localized-content';
+import {
+ PublicPageShell,
+ TextSection,
+} from '@/components/seo/public-page-shell';
import { oosuProjects } from '@/lib/oosu-profile';
import { createPageMetadata } from '@/lib/seo';
@@ -13,14 +17,35 @@ export const metadata: Metadata = createPageMetadata({
keywords: ['Oosu projects', 'AskOosu project', 'RAG portfolio project'],
});
+const projectDescriptionsKo: Record = {
+ 'AskOosu 2026':
+ 'Mac mini 홈 서버에 배포한 대화형 AI 포트폴리오입니다. Next.js, FAQ 라우팅, Notion RAG, PostgreSQL 기반 근거를 연결해 프로젝트와 기술에 답합니다.',
+ Aigram:
+ '피드, 팔로우, 댓글, 백엔드 API 흐름을 구현한 풀스택 SNS 프로젝트입니다.',
+ 'Sticks & Stones Homepage':
+ '기존 WordPress 사이트를 TypeScript와 Vite 기반으로 이전·리뉴얼한 실제 기업 홈페이지 프로젝트입니다.',
+ 'Portfoli-Oh! 2025':
+ '인터랙션, 모션, UI/UX 실험을 통해 프로젝트와 이야기를 보여주는 2025년 프론트엔드 포트폴리오입니다.',
+ Pylingo:
+ '기초 문법부터 응용 문제까지 브라우저에서 실습하는 인터랙티브 Python 학습 웹 앱입니다.',
+ Javalingo:
+ '객체지향 개념과 코딩 테스트 학습을 단계별로 구성한 Java 학습 웹 앱입니다.',
+};
+
export default function ProjectsPage() {
return (
-
+
+
+
+ {blogAuthor.displayName}
+
+ 라는 이름으로 활동하는 AI·풀스택 개발자 장우수의 블로그입니다.
+
+
+ This is the blog of Jang Oosu, an AI-connected fullstack developer
+ working as{' '}
+
+ oosuhada
+
+ .
+
+