{"locale":"ko","source":{"rawBase":"https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/docs","githubBase":"https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs","editBase":"https://github.com/earendil-works/pi/edit/main/packages/coding-agent/docs"},"redirects":[{"from":"/docs/latest/session","to":"/docs/latest/session-format"},{"from":"/docs/latest/tree","to":"/docs/latest/sessions"}],"fileToSlug":{"compaction.md":"compaction","containerization.md":"containerization","custom-provider.md":"custom-provider","development.md":"development","environment-variables.md":"environment-variables","extensions.md":"extensions","index.md":"index","json.md":"json","keybindings.md":"keybindings","llama-cpp.md":"llama-cpp","models.md":"models","packages.md":"packages","prompt-templates.md":"prompt-templates","providers.md":"providers","quickstart.md":"quickstart","rpc.md":"rpc","sdk.md":"sdk","security.md":"security","session-format.md":"session-format","sessions.md":"sessions","settings.md":"settings","shell-aliases.md":"shell-aliases","skills.md":"skills","terminal-setup.md":"terminal-setup","termux.md":"termux","themes.md":"themes","tmux.md":"tmux","tui.md":"tui","usage.md":"usage","windows.md":"windows"},"pages":{"ko":{"compaction":{"title":"압축 및 분기 요약","markdown":"LLM에는 제한된 컨텍스트 창이 있습니다. 대화가 너무 길어지면 Pi는 압축을 사용하여 최근 작업을 유지하면서 오래된 콘텐츠를 요약합니다. 이 페이지에서는 자동 압축과 branch summarization을 모두 다룹니다.\n\n**소스 파일**([pi-mono](https://github.com/earendil-works/pi-mono)):\n- [`packages/coding-agent/src/core/compaction/compaction.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) - 자동 압축 논리\n- [`packages/coding-agent/src/core/compaction/branch-summarization.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts) - 지점 요약\n- [`packages/coding-agent/src/core/compaction/utils.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/utils.ts) - 공유 유틸리티(파일 추적, 직렬화)\n- [`packages/coding-agent/src/core/session-manager.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/session-manager.ts) - 항목 유형(`CompactionEntry`, `BranchSummaryEntry`)\n- [`packages/coding-agent/src/core/extensions/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/extensions/types.ts) - 확장 이벤트 유형\n\n프로젝트의 TypeScript 정의에 대해서는 `node_modules/@earendil-works/pi-coding-agent/dist/`를 검사하세요.\n\n## 개요\n\nPi에는 두 가지 요약 메커니즘이 있습니다.\n\n| 기구 | 방아쇠 | 목적 |\n|-----------|---------|---------|\n| 압축 | 컨텍스트가 임계값을 초과하거나 `/compact` | 오래된 메시지를 요약하여 맥락을 확보하세요 |\n| 지점 요약 | `/tree` 탐색 | 지점 전환 시 컨텍스트 유지 |\n\n둘 다 동일한 구조화된 요약 형식을 사용하고 파일 작업을 누적적으로 추적합니다. 압축 및 분기 요약 요청은 새로운 라우팅 세션 ID를 사용하며, 공급자가 지원하는 경우 이러한 일회성 프롬프트는 재사용될 가능성이 없으므로 프롬프트 캐시 쓰기를 비활성화합니다.\n\n## 압축\n\n### 트리거될 때\n\n자동 압축은 다음과 같은 경우에 트리거됩니다.\n\n```\ncontextTokens > contextWindow - reserveTokens\n```\n\n기본적으로 `reserveTokens`는 16384개의 토큰입니다(`~/.pi/agent/settings.json` 또는 `<project-dir>/.pi/settings.json`에서 구성 가능). 이는 LLM의 대응을 위한 여지를 남겨둡니다.\n\n`/compact [instructions]`를 사용하여 수동으로 트리거할 수도 있습니다. 여기서 선택적 지침은 요약에 초점을 맞춥니다.\n\n### 작동 방식\n\n1. **절단점 찾기**: 최신 메시지에서 뒤로 이동하여 `keepRecentTokens`(기본값 20k, `~/.pi/agent/settings.json` 또는 `<project-dir>/.pi/settings.json`에서 구성 가능)에 도달할 때까지 토큰 추정치를 누적합니다.\n2. **메시지 추출**: 이전 보관된 경계(또는 세션 시작)부터 절단 지점까지의 메시지를 수집합니다.\n3. **요약 생성**: LLM을 호출하여 구조화된 형식으로 요약하고 이전 요약이 있는 경우 반복 컨텍스트로 전달합니다.\n4. **항목 추가**: 요약과 함께 `CompactionEntry` 및 `firstKeptEntryId`를 저장합니다.\n5. **새로고침**: `firstKeptEntryId` 이후의 요약 + 메시지를 사용하여 세션을 다시 로드합니다.\n\n```\nBefore compaction:\n\n  entry:  0     1     2     3      4     5     6      7      8     9\n        ┌─────┬─────┬─────┬─────┬──────┬─────┬──── ─┬──────┬─────┬─────┐\n        │ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool│\n        └─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴─────┘\n                └────────┬───────┘ └──────────────┬──────────────┘\n               messagesToSummarize            kept messages\n                                   ↑\n                          firstKeptEntryId (entry 4)\n\nAfter compaction (new entry appended):\n\n  entry:  0     1     2     3      4     5     6      7      8     9     10\n        ┌─────┬─────┬─────┬─────┬──────┬─────┬──── ─┬──────┬─────┬─────┬─────┐\n        │ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool│ cmp │\n        └─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴─────┴─────┘\n               └──────────┬──────┘ └──────────────────────┬───────────────────┘\n                 not sent to LLM                    sent to LLM\n                                                         ↑\n                                              starts from firstKeptEntryId\n\nWhat the LLM sees:\n\n  ┌────────┬─────────┬─────┬─────┬──────┬──────┬─────┬──────┐\n  │ system │ summary │ usr │ ass │ tool │ tool │ ass │ tool │\n  └────────┴─────────┴─────┴─────┴──────┴──────┴─────┴──────┘\n       ↑         ↑      └─────────────────┬────────────────┘\n    prompt   from cmp          messages from firstKeptEntryId\n```\n\n반복되는 압축에서 요약된 범위는 압축 항목 자체가 아닌 이전 압축의 유지된 경계(`firstKeptEntryId`)에서 시작하며, 유지된 항목을 경로에서 찾을 수 없는 경우 이전 압축 이후의 항목으로 돌아갑니다. 이렇게 하면 다음 요약 단계에도 포함되어 이전 압축에서 살아남은 메시지가 보존됩니다. Pi는 또한 새 `CompactionEntry`를 작성하기 전에 재구축된 세션 컨텍스트에서 `tokensBefore`를 다시 계산하므로 토큰 수는 대체되는 실제 압축 전 컨텍스트를 반영합니다.\n\n### 분할 회전\n\n\"턴\"은 사용자 메시지로 시작되며 다음 사용자 메시지까지 모든 보조자 ​​응답 및 도구 호출을 포함합니다. 일반적으로 압축은 회전 경계에서 절단됩니다.\n\n단일 회전이 `keepRecentTokens`를 초과하면 보조 메시지에 따라 컷 포인트가 회전 중간에 도착합니다. 이것이 \"분할 회전\"입니다.\n\n```\nSplit turn (one huge turn exceeds budget):\n\n  entry:  0     1     2      3     4      5      6     7      8\n        ┌─────┬─────┬─────┬──────┬─────┬──────┬──────┬─────┬──────┐\n        │ hdr │ usr │ ass │ tool │ ass │ tool │ tool │ ass │ tool │\n        └─────┴─────┴─────┴──────┴─────┴──────┴──────┴─────┴──────┘\n                ↑                                     ↑\n         turnStartIndex = 1                  firstKeptEntryId = 7\n                │                                     │\n                └──── turnPrefixMessages (1-6) ───────┘\n                                                      └── kept (7-8)\n\n  isSplitTurn = true\n  messagesToSummarize = []  (no complete turns before)\n  turnPrefixMessages = [usr, ass, tool, ass, tool, tool]\n```\n\n분할 회전의 경우 Pi는 두 개의 요약을 생성하고 병합합니다.\n1. **기록 요약**: 이전 컨텍스트(있는 경우)\n2. **턴 프리픽스 요약**: 스플릿 턴의 초기 부분\n\n### 컷 포인트 규칙\n\n유효한 절단점은 다음과 같습니다.\n- 사용자 메시지\n- 어시스턴트 메시지\n- BashExecution 메시지\n- 사용자 정의 메시지(custom_message, Branch_summary)\n\n도구 결과를 자르지 마십시오(도구 호출을 유지해야 함).\n\n### 압축항목 구조\n\n[`session-manager.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/session-manager.ts)에 정의됨:\n\n```typescript\ninterface CompactionEntry<T = unknown> {\n  type: \"compaction\";\n  id: string;\n  parentId: string;\n  timestamp: number;\n  summary: string;\n  firstKeptEntryId: string;\n  tokensBefore: number;\n  usage?: Usage;       // LLM usage that generated the summary\n  fromHook?: boolean;  // true if provided by extension (legacy field name)\n  details?: T;         // implementation-specific data\n}\n\n// Default compaction uses this for details (from compaction.ts):\ninterface CompactionDetails {\n  readFiles: string[];\n  modifiedFiles: string[];\n}\n```\n\nExtensions는 `details`에 JSON 직렬화 가능한 데이터를 저장할 수 있습니다. 기본 압축은 파일 작업을 추적하지만 사용자 정의 확장 구현은 자체 구조를 사용할 수 있습니다. 생성 및 확장 제공 요약은 사용 가능한 경우 LLM `usage`을 저장하므로 세션 총계에는 요약 작업이 포함됩니다.\n\n구현 방법은 [`prepareCompaction()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) 및 [`compact()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/compaction.ts)를 참조하세요. 직접 프로그래밍 방식 요약의 경우 `generateSummary()`는 요약 텍스트를 반환하고 `generateSummaryWithUsage()`는 `{ text, usage }`를 반환합니다.\n\n## 지점 요약\n\n### 트리거될 때\n\n`/tree`를 사용하여 다른 분기로 이동하면 Pi를 사용하면 종료할 작업을 요약할 수 있습니다. 이렇게 하면 왼쪽 분기의 컨텍스트가 새 분기에 주입됩니다.\n\n### 작동 방식\n\n1. **공통 조상 찾기**: 이전 위치와 새 위치가 공유하는 가장 깊은 노드\n2. **항목 수집**: 오래된 잎에서 다시 공통 조상까지 걸어갑니다.\n3. **예산으로 준비**: 토큰 예산까지 메시지 포함(최신순)\n4. **요약 생성**: 구조화된 형식으로 LLM 호출\n5. **항목 추가**: 탐색 지점에 `BranchSummaryEntry` 저장\n\n```\nTree before navigation:\n\n         ┌─ B ─ C ─ D (old leaf, being abandoned)\n    A ───┤\n         └─ E ─ F (target)\n\nCommon ancestor: A\nEntries to summarize: B, C, D\n\nAfter navigation with summary:\n\n         ┌─ B ─ C ─ D\n    A ───┤\n         └─ E ─ F ─ [summary of B,C,D] (new leaf)\n```\n\n### 누적 파일 추적\n\n압축과 branch summarization 모두 파일을 누적적으로 추적합니다. 요약을 생성할 때 pi는 다음에서 파일 작업을 추출합니다.\n- 요약되는 메시지의 도구 호출\n- 이전 압축 또는 분기 요약 `details` (있는 경우)\n\n이는 파일 추적이 여러 압축 또는 중첩된 분기 요약에 걸쳐 누적되어 읽기 및 수정된 파일의 전체 기록을 보존한다는 것을 의미합니다.\n\n### BranchSummary항목 구조\n\n[`session-manager.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/session-manager.ts)에 정의됨:\n\n```typescript\ninterface BranchSummaryEntry<T = unknown> {\n  type: \"branch_summary\";\n  id: string;\n  parentId: string;\n  timestamp: number;\n  summary: string;\n  fromId: string;      // Entry we navigated from\n  usage?: Usage;       // LLM usage that generated the summary\n  fromHook?: boolean;  // true if provided by extension (legacy field name)\n  details?: T;         // implementation-specific data\n}\n\n// Default branch summarization uses this for details (from branch-summarization.ts):\ninterface BranchSummaryDetails {\n  readFiles: string[];\n  modifiedFiles: string[];\n}\n```\n\n압축과 마찬가지로 확장 프로그램은 `details`에 사용자 정의 데이터를 저장할 수 있습니다.\n\n구현 방법은 [`collectEntriesForBranchSummary()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts), [`prepareBranchEntries()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts) 및 [`generateBranchSummary()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts)를 참조하세요.\n\n## 요약 형식\n\n압축과 branch summarization 모두 동일한 구조 형식을 사용합니다.\n\n```markdown\n## Goal\n[What the user is trying to accomplish]\n\n## Constraints & Preferences\n- [Requirements mentioned by user]\n\n## Progress\n### Done\n- [x] [Completed tasks]\n\n### In Progress\n- [ ] [Current work]\n\n### Blocked\n- [Issues, if any]\n\n## Key Decisions\n- **[Decision]**: [Rationale]\n\n## Next Steps\n1. [What should happen next]\n\n## Critical Context\n- [Data needed to continue]\n\n<read-files>\npath/to/file1.ts\npath/to/file2.ts\n</read-files>\n\n<modified-files>\npath/to/changed.ts\n</modified-files>\n```\n\n### 메시지 직렬화\n\n요약하기 전에 메시지는 [`serializeConversation()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/utils.ts)를 통해 텍스트로 직렬화됩니다.\n\n```\n[User]: What they said\n[Assistant thinking]: Internal reasoning\n[Assistant]: Response text\n[Assistant tool calls]: read(path=\"foo.ts\"); edit(path=\"bar.ts\", ...)\n[Tool result]: Output from tool\n```\n\n이렇게 하면 모델이 이를 대화로 처리하여 계속할 수 없습니다.\n\n도구 결과는 직렬화 중에 2000자로 잘립니다. 해당 제한을 초과하는 콘텐츠는 잘린 문자 수를 나타내는 표시로 대체됩니다. 도구 결과(특히 `read` 및 `bash`)가 일반적으로 컨텍스트 크기에 가장 큰 영향을 미치기 때문에 이렇게 하면 합당한 토큰 예산 내에서 요약 요청이 유지됩니다.\n\n## Extensions를 통한 맞춤 요약\n\nExtensions는 압축과 branch summarization를 모두 가로채고 사용자 정의할 수 있습니다. 이벤트 유형 정의는 [`extensions/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/extensions/types.ts)를 참조하세요.\n\n### session_before_compact\n\n자동 압축 또는 `/compact` 전에 실행됩니다. 취소하거나 사용자 정의 요약을 제공할 수 있습니다. 유형 파일의 `SessionBeforeCompactEvent` 및 `CompactionPreparation`를 참조하세요.\n\n```typescript\npi.on(\"session_before_compact\", async (event, ctx) => {\n  const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event;\n\n  // preparation.messagesToSummarize - messages to summarize\n  // preparation.turnPrefixMessages - split turn prefix (if isSplitTurn)\n  // preparation.previousSummary - previous compaction summary\n  // preparation.fileOps - extracted file operations\n  // preparation.tokensBefore - context tokens before compaction\n  // preparation.firstKeptEntryId - where kept messages start\n  // preparation.settings - compaction settings\n\n  // branchEntries - all entries on current branch (for custom state)\n  // reason - \"manual\" (/compact), \"threshold\", or \"overflow\"\n  // willRetry - whether the aborted turn is retried after compaction (overflow recovery)\n  // signal - AbortSignal (pass to LLM calls)\n\n  // Cancel:\n  return { cancel: true };\n\n  // Custom summary:\n  return {\n    compaction: {\n      summary: \"Your summary...\",\n      firstKeptEntryId: preparation.firstKeptEntryId,\n      tokensBefore: preparation.tokensBefore,\n      // usage: summaryResponse.usage, // Optional; included in session totals\n      details: { /* custom data */ },\n    }\n  };\n});\n```\n\n#### 메시지를 텍스트로 변환\n\n자신만의 모델로 요약을 생성하려면 `serializeConversation`를 사용하여 메시지를 텍스트로 변환하세요.\n\n```typescript\nimport { convertToLlm, serializeConversation } from \"@earendil-works/pi-coding-agent\";\n\npi.on(\"session_before_compact\", async (event, ctx) => {\n  const { preparation } = event;\n  \n  // Convert AgentMessage[] to Message[], then serialize to text\n  const conversationText = serializeConversation(\n    convertToLlm(preparation.messagesToSummarize)\n  );\n  // Returns:\n  // [User]: message text\n  // [Assistant thinking]: thinking content\n  // [Assistant]: response text\n  // [Assistant tool calls]: read(path=\"...\"); bash(command=\"...\")\n  // [Tool result]: output text\n\n  // Now send to your model for summarization\n  const { summary, usage } = await myModel.summarize(conversationText);\n  \n  return {\n    compaction: {\n      summary,\n      firstKeptEntryId: preparation.firstKeptEntryId,\n      tokensBefore: preparation.tokensBefore,\n      usage,\n    }\n  };\n});\n```\n\n다른 모델을 사용한 전체 예는 [custom-compaction.ts](../examples/extensions/custom-compaction.ts)를 참조하세요.\n\n### session_before_tree\n\n`/tree` 탐색 전에 실행됩니다. 사용자가 요약을 선택했는지 여부에 관계없이 항상 실행됩니다. 탐색을 취소하거나 사용자 정의 요약을 제공할 수 있습니다.\n\n```typescript\npi.on(\"session_before_tree\", async (event, ctx) => {\n  const { preparation, signal } = event;\n\n  // preparation.targetId - where we're navigating to\n  // preparation.oldLeafId - current position (being abandoned)\n  // preparation.commonAncestorId - shared ancestor\n  // preparation.entriesToSummarize - entries that would be summarized\n  // preparation.userWantsSummary - whether user chose to summarize\n\n  // Cancel navigation entirely:\n  return { cancel: true };\n\n  // Provide custom summary (only used if userWantsSummary is true):\n  if (preparation.userWantsSummary) {\n    return {\n      summary: {\n        summary: \"Your summary...\",\n        // usage: summaryResponse.usage, // Optional; included in session totals\n        details: { /* custom data */ },\n      }\n    };\n  }\n});\n```\n\n유형 파일의 `SessionBeforeTreeEvent` 및 `TreePreparation`를 참조하세요.\n\n## 설정\n\n`~/.pi/agent/settings.json` 또는 `<project-dir>/.pi/settings.json`에서 압축을 구성합니다.\n\n```json\n{\n  \"compaction\": {\n    \"enabled\": true,\n    \"reserveTokens\": 16384,\n    \"keepRecentTokens\": 20000\n  }\n}\n```\n\n| 환경 | 기본 | 설명 |\n|---------|---------|-------------|\n| `enabled` | `true` | 자동 압축 활성화 |\n| `reserveTokens` | `16384` | LLM 응답을 위해 예약할 토큰 |\n| `keepRecentTokens` | `20000` | 보관할 최근 토큰(요약되지 않음) |\n\n`\"enabled\": false`로 자동 압축을 비활성화합니다. `/compact`를 사용하여 수동으로 압축할 수도 있습니다.","sourceFile":"compaction.md"},"containerization":{"title":"컨테이너화","markdown":"Pi는 기본적으로 모든 권한으로 실행되지만 경우에 따라 Pi이 쓸 수 있는 디렉터리와 액세스할 수 있는 디렉터리를 더 세밀하게 제어하고 싶을 수도 있습니다.\n\n두 가지 일반 옵션이 있습니다. 다음 중 하나를 수행할 수 있습니다.\n1. 격리된 환경 내에서 전체 `pi` 프로세스를 실행하거나\n2. 호스트에서 `pi`를 실행하고 도구 실행을 격리된 환경으로 라우팅합니다.\n\n## 패턴을 선택하세요\n\n| 무늬 | 고립된 것은 무엇인가 | 다음에 가장 적합 | 메모 |\n| --- | --- | --- | --- |\n| Gondolin 확장자 | 내장 도구 및 `!` 명령 | 호스트에서 인증을 유지하면서 로컬 마이크로 VM 격리 | [`examples/extensions/gondolin/`](../examples/extensions/gondolin/)를 참조하세요. |\n| 일반 Docker | 로컬 컨테이너의 전체 `pi` 프로세스 | 간단한 지역 격리 | 공급자 API key가 컨테이너에 들어갑니다. |\n| OpenShell | 정책에 따라 제어되는 sandbox의 전체 `pi` 프로세스 | 로컬 또는 원격 관리 sandbox | OpenShell 게이트웨이 필요 |\n\nExtensions `pi` 프로세스가 실행되는 곳마다 실행됩니다. 도구 라우팅 확장을 사용하여 호스트 `pi`를 실행하는 경우 다른 사용자 정의 확장 도구가 작업을 위임하지 않는 한 호스트에서 계속 실행됩니다.\n\n## Gondolin\n\n[Gondolin](https://github.com/earendil-works/gondolin)은 로컬 Linux 마이크로 VM입니다.\n호스트에서 `pi`를 원하지만 모든 내장 도구가 VM으로 라우팅되는 경우 [example extension](../examples/extensions/gondolin)를 사용하세요.\n\n설정:\n\n```bash\ncp -R packages/coding-agent/examples/extensions/gondolin ~/.pi/agent/extensions/gondolin\ncd ~/.pi/agent/extensions/gondolin\nnpm install --ignore-scripts\n```\n\n마운트하려는 프로젝트에서 실행하십시오.\n\n```bash\ncd /path/to/project\npi -e ~/.pi/agent/extensions/gondolin\n```\n\n확장은 VM의 `/workspace`에 호스트 cwd를 마운트하고 `read`, `write`, `edit`, `bash`, `grep`, `find` 및 `ls`를 재정의합니다.\n사용자 `!` 명령도 VM으로 라우팅됩니다.\n`/workspace` 아래의 파일 변경 사항은 호스트에 기록됩니다.\n\n요구 사항: `@earendil-works/gondolin`의 경우 Node.js >= 23.6.0 및 QEMU(패키지 관리자를 통한 설치 필요).\n\n## 일반 Docker\n\n가장 간단한 로컬 컨테이너 경계를 원할 경우 Docker에서 전체 `pi` 프로세스를 실행하세요.\n\n`Dockerfile.pi`:\n\n```dockerfile\nFROM node:24-bookworm-slim\n\nRUN apt-get update \\\n  && apt-get install -y --no-install-recommends bash ca-certificates git ripgrep \\\n  && rm -rf /var/lib/apt/lists/*\nRUN npm install -g --ignore-scripts @earendil-works/pi-coding-agent\n\nWORKDIR /workspace\nENTRYPOINT [\"pi\"]\n```\n\n빌드 및 실행:\n\n```bash\ndocker build -t pi-sandbox -f Dockerfile.pi .\n\ndocker run --rm -it \\\n  -e ANTHROPIC_API_KEY \\\n  -v \"$PWD:/workspace\" \\\n  -v pi-agent-home:/root/.pi/agent \\\n  pi-sandbox\n```\n\n`-v \"$PWD:/workspace\"`는 현재 디렉터리를 /workspace의 컨테이너에 마운트하여 Docker 내부의 `/workspace` 읽기 및 쓰기가 Gondolin 예와 같이 호스트 파일에 직접 영향을 미치도록 합니다.\n\n컨테이너-로컬 설정 및 세션을 원하는 경우 `/root/.pi/agent`에 명명된 볼륨을 사용하세요. 호스트를 마운트하면 `~/.pi/agent` 호스트 인증 및 세션 파일이 컨테이너에 노출됩니다.\n\n## OpenShell\n\n파일 시스템, 프로세스, 네트워크, 자격 증명 및 추론 제어를 통해 정책 제어 sandbox를 원할 경우 [NVIDIA OpenShell](https://docs.nvidia.com/openshell/about/overview)를 사용하세요.\nOpenShell는 Docker가 지원하는 로컬 게이트웨이, Podman 또는 VM 런타임을 통해 또는 원격 Kubernetes 게이트웨이를 통해 sandboxes를 실행할 수 있습니다.\n\n모든 sandbox에는 활성 게이트웨이가 필요합니다.\nsandbox을 만들기 전에 하나를 등록하고 선택하세요.\n\n```bash\nopenshell gateway add <gateway-url> --name <name>\nopenshell gateway select <name>\n```\n\nOpenShell sandbox 내부에서 `pi` 실행:\n\n```bash\nopenshell sandbox create --name pi-sandbox --from pi -- pi\n```\n\n이 패턴에서는 전체 `pi` 프로세스가 sandbox 내부에서 실행됩니다.\n내장 도구, `!` 명령 및 확장 도구는 OpenShell 경계 내에서 실행됩니다.\n\n게이트웨이가 원격인 경우 프로젝트 파일은 호스트에서 바인드 마운트되지 않습니다. 즉, sandbox에 쓴 내용이 컴퓨터에 반영되지 않습니다.\nsandbox 내부에 저장소를 복제하거나 OpenShell 파일 전송 명령을 사용하세요.\n\n```bash\nopenshell sandbox upload pi-sandbox ./repo /workspace\nopenshell sandbox download pi-sandbox /workspace/repo ./repo-out\n```\n\nOpenShell 제공자는 sandbox 외부에 원시 모델 API key을 유지할 수 있습니다.\n추론 라우팅이 구성되면 sandbox 내부의 코드가 `https://inference.local`를 호출할 수 있고 게이트웨이는 구성된 공급자 자격 증명 업스트림을 주입합니다.\n모델 트래픽이 이 경로를 사용하도록 하려면 해당 OpenAI 호환 또는 Anthropic 호환 엔드포인트를 사용하도록 Pi를 구성하세요.","sourceFile":"containerization.md"},"custom-provider":{"title":"맞춤 Providers","markdown":"Extensions는 `pi.registerProvider()`를 통해 맞춤 모델 제공자를 등록할 수 있습니다. 이를 통해 다음이 가능해집니다.\n\n- **프록시** - 기업 프록시 또는 API 게이트웨이를 통해 요청 라우팅\n- **사용자 정의 엔드포인트** - 자체 호스팅 또는 비공개 모델 배포 사용\n- **OAuth/SSO** - 엔터프라이즈 공급자를 위한 인증 흐름 추가\n- **맞춤 APIs** - 비표준 LLM APIs에 대한 스트리밍 구현\n\n## 예시 Extensions\n\n다음 전체 공급자 예를 참조하세요.\n\n- [`examples/extensions/custom-provider-anthropic/`](../examples/extensions/custom-provider-anthropic/)\n- [`examples/extensions/custom-provider-gitlab-duo/`](../examples/extensions/custom-provider-gitlab-duo/)\n\n## 목차\n\n- [Example Extensions](#example-extensions)\n- [Quick Reference](#quick-reference)\n- [Override Existing Provider](#override-existing-provider)\n- [Register New Provider](#register-new-provider)\n- [Unregister Provider](#unregister-provider)\n- [OAuth Support](#oauth-support)\n- [Custom Streaming API](#custom-streaming-api)\n- [Context Overflow Errors](#context-overflow-errors)\n- [Testing Your Implementation](#testing-your-implementation)\n- [Config Reference](#config-reference)\n- [Model Definition Reference](#model-definition-reference)\n\n## 빠른 참조\n\nExtensions는 완전한 pi-ai `Provider`를 등록하거나 기존 공급자 구성 양식을 사용할 수 있습니다. 사용자 정의 인증, 필터링, 새로 고침 또는 스트리밍 동작이 필요한 경우 완전한 공급자를 선호하십시오. Pi 구성 `models.json` 위의 등록된 기본 공급자를 재정의합니다.\n\n```typescript\nimport { createProvider, openAICompletionsApi } from \"@earendil-works/pi-ai\";\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerProvider(createProvider({\n    id: \"native-local\",\n    name: \"Native Local\",\n    baseUrl: \"http://localhost:8080/v1\",\n    auth: {\n      apiKey: {\n        name: \"Local server API key\",\n        async login(interaction) {\n          return {\n            type: \"api_key\",\n            key: await interaction.prompt({ type: \"secret\", message: \"API key\" })\n          };\n        },\n        async resolve({ credential }) {\n          return credential?.key\n            ? { auth: { apiKey: credential.key }, source: \"stored API key\" }\n            : undefined;\n        }\n      }\n    },\n    models: [],\n    api: openAICompletionsApi()\n  }));\n\n  // Legacy provider-config form:\n  // Override baseUrl for existing provider\n  pi.registerProvider(\"anthropic\", {\n    baseUrl: \"https://proxy.example.com\"\n  });\n\n  // Register new provider with models\n  pi.registerProvider(\"my-provider\", {\n    name: \"My Provider\",\n    baseUrl: \"https://api.example.com\",\n    apiKey: \"$MY_API_KEY\",\n    api: \"openai-completions\",\n    models: [\n      {\n        id: \"my-model\",\n        name: \"My Model\",\n        reasoning: false,\n        input: [\"text\", \"image\"],\n        cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n        contextWindow: 128000,\n        maxTokens: 4096\n      }\n    ]\n  });\n}\n```\n\n확장 팩토리는 `async`일 수도 있습니다. 동적 모델 검색을 위해서는 `session_start` 대신 공장에서 모델을 가져와 등록하세요. pi는 시작이 계속되기 전에 팩토리를 기다리므로 대화형 시작 및 `pi --list-models` 동안 공급자를 사용할 수 있습니다.\n\n## 기존 공급자 재정의\n\n가장 간단한 사용 사례: 프록시를 통해 기존 공급자를 리디렉션합니다.\n\n```typescript\n// All Anthropic requests now go through your proxy\npi.registerProvider(\"anthropic\", {\n  baseUrl: \"https://proxy.example.com\"\n});\n\n// Add custom headers to OpenAI requests\npi.registerProvider(\"openai\", {\n  headers: {\n    \"X-Custom-Header\": \"value\"\n  }\n});\n\n// Both baseUrl and headers\npi.registerProvider(\"google\", {\n  baseUrl: \"https://ai-gateway.corp.com/google\",\n  headers: {\n    \"X-Corp-Auth\": \"$CORP_AUTH_TOKEN\"  // env var or literal\n  }\n});\n```\n\n`baseUrl` 및/또는 `headers`만 제공되는 경우(`models` 없음) 해당 공급자의 모든 기존 모델은 새 엔드포인트와 함께 보존됩니다.\n\n## 새로운 공급자 등록\n\n완전히 새로운 공급자를 추가하려면 필수 구성과 함께 `models`를 지정하세요.\n\n모델 목록이 원격 엔드포인트에서 제공되는 경우 비동기 확장 팩토리를 사용하세요.\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default async function (pi: ExtensionAPI) {\n  const response = await fetch(\"http://localhost:1234/v1/models\");\n  const payload = (await response.json()) as {\n    data: Array<{\n      id: string;\n      name?: string;\n      context_window?: number;\n      max_tokens?: number;\n    }>;\n  };\n\n  pi.registerProvider(\"local-openai\", {\n    baseUrl: \"http://localhost:1234/v1\",\n    apiKey: \"$LOCAL_OPENAI_API_KEY\",\n    api: \"openai-completions\",\n    models: payload.data.map((model) => ({\n      id: model.id,\n      name: model.name ?? model.id,\n      reasoning: false,\n      input: [\"text\"],\n      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n      contextWindow: model.context_window ?? 128000,\n      maxTokens: model.max_tokens ?? 4096,\n    })),\n  });\n}\n```\n\n시작이 완료되기 전에 가져온 모델을 등록합니다.\n\n```typescript\npi.registerProvider(\"my-llm\", {\n  baseUrl: \"https://api.my-llm.com/v1\",\n  apiKey: \"$MY_LLM_API_KEY\",  // env var reference\n  api: \"openai-completions\",  // which streaming API to use\n  models: [\n    {\n      id: \"my-llm-large\",\n      name: \"My LLM Large\",\n      reasoning: true,        // supports extended thinking\n      input: [\"text\", \"image\"],\n      cost: {\n        input: 3.0,           // $/million tokens\n        output: 15.0,\n        cacheRead: 0.3,\n        cacheWrite: 3.75\n      },\n      contextWindow: 200000,\n      maxTokens: 16384\n    }\n  ]\n});\n```\n\n`models`가 제공되면 해당 공급자의 기존 모델을 모두 **교체**합니다.\n\n`apiKey` 및 사용자 정의 헤더 값은 `models.json`와 동일한 구성 값 구문을 사용합니다. `!command`는 시작 시 전체 값에 대한 명령을 실행하고, `$ENV_VAR` 및 `${ENV_VAR}`는 환경 변수를 보간하고, `$`는 리터럴 ``apiKey` 및 사용자 정의 헤더 값은 `models.json`와 동일한 구성 값 구문을 사용합니다. `!command`는 시작 시 전체 값에 대한 명령을 실행하고, `$ENV_VAR` 및 `${ENV_VAR}`는 환경 변수를 보간하고, `$`는 리터럴 을 내보내고, `$!`는 리터럴을 내보냅니다. `!`.\n\n## 공급자 등록 취소\n\n이전에 `pi.registerProvider(name,...)`를 통해 등록된 제공자를 제거하려면 `pi.unregisterProvider(name)`를 사용하세요.\n\n```typescript\n// Register\npi.registerProvider(\"my-llm\", {\n  baseUrl: \"https://api.my-llm.com/v1\",\n  apiKey: \"$MY_LLM_API_KEY\",\n  api: \"openai-completions\",\n  models: [\n    {\n      id: \"my-llm-large\",\n      name: \"My LLM Large\",\n      reasoning: true,\n      input: [\"text\", \"image\"],\n      cost: { input: 3.0, output: 15.0, cacheRead: 0.3, cacheWrite: 3.75 },\n      contextWindow: 200000,\n      maxTokens: 16384\n    }\n  ]\n});\n\n// Later, remove it\npi.unregisterProvider(\"my-llm\");\n```\n\n등록을 취소하면 해당 공급자의 동적 모델, API key 대체, OAuth 공급자 등록 및 사용자 정의 스트림 처리기 등록이 제거됩니다. 재정의된 모든 기본 제공 모델 또는 공급자 동작이 복원됩니다.\n\n초기 확장 로드 단계 이후에 이루어진 호출은 즉시 적용되므로 `/reload`가 필요하지 않습니다.\n\n### API 유형\n\n`api` 필드는 어떤 스트리밍 구현이 사용되는지 결정합니다.\n\n| API | 용도 |\n|-----|---------|\n| `anthropic-messages` | Anthropic Claude API 및 호환 제품 |\n| `openai-completions` | OpenAI 채팅 완료 API 및 호환 항목 |\n| `openai-responses` | OpenAI 응답 API |\n| `azure-openai-responses` | Azure OpenAI 응답 API |\n| `openai-codex-responses` | OpenAI 코덱스 응답 API |\n| `mistral-conversations` | 기본 Mistral 채팅 완료 스트리밍 |\n| `google-generative-ai` | Google 생성 AI API |\n| `google-vertex` | Google Vertex AI API |\n| `bedrock-converse-stream` | 아마존 베드락 컨버스 API |\n\n대부분의 OpenAI 호환 공급자는 `openai-completions`와 함께 작동합니다. 모델별 사고 수준에는 모델 수준 `thinkingLevelMap`을 사용하고 공급자의 특이 사항에는 `compat`를 사용하세요. `xhigh` 및 `max` 수준은 선택 가능하고 null이 아닌 지도 항목이 필요하며 지원되지 않는 구멍으로 구분될 수 있습니다.\n\n```typescript\nmodels: [{\n  id: \"custom-model\",\n  // ...\n  reasoning: true,\n  thinkingLevelMap: {              // map pi levels to provider values; null hides unsupported levels\n    minimal: null,\n    low: null,\n    medium: null,\n    high: \"default\",\n    xhigh: null,\n    max: \"max\"\n  },\n  compat: {\n    supportsDeveloperRole: false,   // use \"system\" instead of \"developer\"\n    supportsReasoningEffort: true,\n    maxTokensField: \"max_tokens\",   // instead of \"max_completion_tokens\"\n    requiresToolResultName: true,   // tool results need name field\n    thinkingFormat: \"qwen\",        // top-level enable_thinking: true\n    cacheControlFormat: \"anthropic\" // Anthropic-style cache_control markers\n  }\n}]\n```\n\nOpenRouter 스타일 `reasoning: { effort }` 컨트롤에는 `openrouter`를 사용하세요. Together 스타일 `reasoning: { enabled }` 컨트롤에는 `together`를 사용하세요. `supportsReasoningEffort`를 사용하면 `reasoning_effort`도 전송됩니다. `chat_template_kwargs.enable_thinking`을 읽고 `preserve_thinking`이 필요한 로컬 Qwen 호환 서버에는 `qwen-chat-template`를 사용하세요.\n시스템 프롬프트, 마지막 도구 정의, 마지막 사용자, 보조자 또는 도구 결과 텍스트 콘텐츠에서 `cache_control`를 통해 Anthropic 스타일 프롬프트 캐싱을 노출하는 OpenAI 호환 공급자의 경우 `cacheControlFormat: \"anthropic\"`를 사용하세요.\n\n`api: \"anthropic-messages\"`를 사용하는 인류 호환 제공자의 경우 업스트림 모델에 적응적 사고가 필요한 모델 또는 제공자에 `compat.forceAdaptiveThinking: true`를 설정합니다(`thinking.type: \"adaptive\"` + `output_config.effort`). 내장된 적응형 Claude 모델은 이를 자동으로 설정합니다. 빈 생각 서명을 내보내고 재생 시 `signature: \"\"`를 기대하는 공급자에 대해서만 `compat.allowEmptySignature: true`를 설정합니다.\n\n> 마이그레이션 참고 사항: Mistral이 `openai-completions`에서 `mistral-conversations`로 이동했습니다.\n> 기본 Mistral 모델에는 `mistral-conversations`를 사용하세요.\n> `openai-completions`를 통해 의도적으로 Mistral 호환/사용자 지정 엔드포인트를 라우팅하는 경우 필요에 따라 `compat` 플래그를 명시적으로 설정하세요.\n\n### 인증 헤더\n\n공급자가 `Authorization: Bearer <key>`를 기대하지만 표준 API를 사용하지 않는 경우 `authHeader: true`를 설정하세요.\n\n```typescript\npi.registerProvider(\"custom-api\", {\n  baseUrl: \"https://api.example.com\",\n  apiKey: \"$MY_API_KEY\",\n  authHeader: true,  // adds Authorization: Bearer header\n  api: \"openai-completions\",\n  models: [...]\n});\n```\n\n각 요청에 대해 키가 확인됩니다. 명시적인 요청 `Authorization` 헤더는 생성된 값보다 우선합니다.\n\n## OAuth 지원\n\n`/login`와 통합되는 OAuth/SSO 인증을 추가하세요.\n\n```typescript\nimport type { OAuthCredentials, OAuthLoginCallbacks } from \"@earendil-works/pi-ai\";\n\npi.registerProvider(\"corporate-ai\", {\n  baseUrl: \"https://ai.corp.com/v1\",\n  api: \"openai-responses\",\n  models: [...],\n  oauth: {\n    name: \"Corporate AI (SSO)\",\n\n    async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n      const method = await callbacks.onSelect({\n        message: \"Select login method:\",\n        options: [\n          { id: \"browser\", label: \"Browser OAuth\" },\n          { id: \"device\", label: \"Device code\" }\n        ]\n      });\n      if (!method) throw new Error(\"Login cancelled\");\n\n      let code: string;\n      if (method === \"device\") {\n        callbacks.onDeviceCode({\n          userCode: \"ABCD-1234\",\n          verificationUri: \"https://sso.corp.com/device\",\n          intervalSeconds: 5,\n          expiresInSeconds: 900\n        });\n        code = await pollDeviceCodeUntilComplete();\n      } else {\n        callbacks.onAuth({ url: \"https://sso.corp.com/authorize?...\" });\n        code = await callbacks.onPrompt({ message: \"Enter SSO code:\" });\n      }\n\n      // Exchange for tokens (your implementation)\n      const tokens = await exchangeCodeForTokens(code);\n\n      return {\n        refresh: tokens.refreshToken,\n        access: tokens.accessToken,\n        expires: Date.now() + tokens.expiresIn * 1000\n      };\n    },\n\n    async refreshToken(credentials: OAuthCredentials, signal: AbortSignal): Promise<OAuthCredentials> {\n      const tokens = await refreshAccessToken(credentials.refresh, signal);\n      return {\n        refresh: tokens.refreshToken ?? credentials.refresh,\n        access: tokens.accessToken,\n        expires: Date.now() + tokens.expiresIn * 1000\n      };\n    },\n\n    getApiKey(credentials: OAuthCredentials): string {\n      return credentials.access;\n    }\n  }\n});\n```\n\n등록 후 사용자는 `/login corporate-ai`를 통해 인증할 수 있습니다.\n\n### OAuth로그인콜백\n\n`callbacks` 객체는 공급자 소유 흐름에 대해 UI 중립적 상호 작용을 제공합니다.\n\n```typescript\ninterface OAuthLoginCallbacks {\n  // Open URL in browser (for OAuth redirects)\n  onAuth(params: { url: string }): void;\n\n  // Show device code (for device authorization flow)\n  onDeviceCode(params: {\n    userCode: string;\n    verificationUri: string;\n    intervalSeconds?: number;\n    expiresInSeconds?: number;\n  }): void;\n\n  // Show transient progress\n  onProgress?(message: string): void;\n\n  // Prompt user for input (for manual token entry)\n  onPrompt(params: { message: string }): Promise<string>;\n\n  // Show an interactive selector, e.g. to choose browser OAuth vs device code\n  onSelect(params: {\n    message: string;\n    options: { id: string; label: string }[];\n  }): Promise<string | undefined>;\n}\n```\n\n### OAuth자격증명\n\n자격 증명은 `~/.pi/agent/auth.json`에서 유지됩니다.\n\n```typescript\ninterface OAuthCredentials {\n  refresh: string;   // Refresh token (for refreshToken())\n  access: string;    // Access token (returned by getApiKey())\n  expires: number;   // Expiration timestamp in milliseconds\n}\n```\n\n## 맞춤 스트리밍 API\n\n비표준 API을 사용하는 제공업체의 경우 `streamSimple`를 구현하세요. 직접 작성하기 전에 기존 공급자 구현을 연구하십시오.\n\n**참조 구현:**\n- [anthropic.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/anthropic.ts) - 인류학적 메시지 API\n- [mistral.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/mistral.ts) - 미스트랄 대화 API\n- [openai-completions.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/openai-completions.ts) - OpenAI 채팅 완료\n- [openai-responses.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/openai-responses.ts) - OpenAI 응답 API\n- [google.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/google.ts) - Google 생성 AI\n- [amazon-bedrock.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/amazon-bedrock.ts) - AWS 기반암\n\n### 스트림 패턴\n\n모든 공급자는 동일한 패턴을 따릅니다.\n\n```typescript\nimport {\n  type AssistantMessage,\n  type AssistantMessageEventStream,\n  type Context,\n  type Model,\n  type SimpleStreamOptions,\n  calculateCost,\n  createAssistantMessageEventStream,\n} from \"@earendil-works/pi-ai\";\n\nfunction streamMyProvider(\n  model: Model<any>,\n  context: Context,\n  options?: SimpleStreamOptions\n): AssistantMessageEventStream {\n  const stream = createAssistantMessageEventStream();\n\n  (async () => {\n    // Initialize output message\n    const output: AssistantMessage = {\n      role: \"assistant\",\n      content: [],\n      api: model.api,\n      provider: model.provider,\n      model: model.id,\n      usage: {\n        input: 0,\n        output: 0,\n        cacheRead: 0,\n        cacheWrite: 0,\n        totalTokens: 0,\n        cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n      },\n      stopReason: \"pending\",\n      timestamp: Date.now(),\n    };\n\n    try {\n      // Push start event\n      stream.push({ type: \"start\", partial: output });\n\n      // Make API request and process response...\n      // Push content events as they arrive and set stopReason from the terminal event.\n      if (output.stopReason === \"pending\") {\n        throw new Error(\"Provider stream ended without a stop reason\");\n      }\n      if (output.stopReason === \"error\" || output.stopReason === \"aborted\") {\n        throw new Error(output.errorMessage || \"An unknown error occurred\");\n      }\n\n      // Push done event\n      stream.push({\n        type: \"done\",\n        reason: output.stopReason,\n        message: output\n      });\n      stream.end();\n    } catch (error) {\n      output.stopReason = options?.signal?.aborted ? \"aborted\" : \"error\";\n      output.errorMessage = error instanceof Error ? error.message : String(error);\n      stream.push({ type: \"error\", reason: output.stopReason, error: output });\n      stream.end();\n    }\n  })();\n\n  return stream;\n}\n```\n\n### 이벤트 유형\n\n`stream.push()`를 통해 다음 순서로 이벤트를 푸시합니다.\n\n1. `{ type: \"start\", partial: output }` - 스트리밍이 시작되었습니다.\n\n2. 콘텐츠 이벤트(반복 가능, 각 블록에 대해 `contentIndex` 추적):\n   - `{ type: \"text_start\", contentIndex, partial }` - 텍스트 블록이 시작되었습니다.\n   - `{ type: \"text_delta\", contentIndex, delta, partial }` - 텍스트 덩어리\n   - `{ type: \"text_end\", contentIndex, content, partial }` - 텍스트 블록이 종료되었습니다.\n   - `{ type: \"thinking_start\", contentIndex, partial }` - 생각이 시작되었습니다\n   - `{ type: \"thinking_delta\", contentIndex, delta, partial }` - 생각 덩어리\n   - `{ type: \"thinking_end\", contentIndex, content, partial }` - 생각이 끝났습니다\n   - `{ type: \"toolcall_start\", contentIndex, partial }` - 도구 호출이 시작되었습니다.\n   - `{ type: \"toolcall_delta\", contentIndex, delta, partial }` - 도구 호출 JSON 청크\n   - `{ type: \"toolcall_end\", contentIndex, toolCall, partial }` - 도구 호출이 종료되었습니다.\n\n3. `{ type: \"done\", reason, message }` 또는 `{ type: \"error\", reason, error }` - 스트림이 종료되었습니다.\n\n각 이벤트의 `partial` 필드에는 현재 `AssistantMessage` 상태가 포함됩니다. 데이터를 받으면 `output.content`를 업데이트한 다음 `output`를 `partial`로 포함합니다.\n\n### 콘텐츠 블록\n\n콘텐츠 블록이 도착하면 `output.content`에 추가하세요.\n\n```typescript\n// Text block\noutput.content.push({ type: \"text\", text: \"\" });\nstream.push({ type: \"text_start\", contentIndex: output.content.length - 1, partial: output });\n\n// As text arrives\nconst block = output.content[contentIndex];\nif (block.type === \"text\") {\n  block.text += delta;\n  stream.push({ type: \"text_delta\", contentIndex, delta, partial: output });\n}\n\n// When block completes\nstream.push({ type: \"text_end\", contentIndex, content: block.text, partial: output });\n```\n\n### 도구 호출\n\n도구 호출에는 JSON 축적 및 구문 분석이 필요합니다.\n\n```typescript\n// Start tool call\noutput.content.push({\n  type: \"toolCall\",\n  id: toolCallId,\n  name: toolName,\n  arguments: {}\n});\nstream.push({ type: \"toolcall_start\", contentIndex: output.content.length - 1, partial: output });\n\n// Accumulate JSON\nlet partialJson = \"\";\npartialJson += jsonDelta;\ntry {\n  block.arguments = JSON.parse(partialJson);\n} catch {}\nstream.push({ type: \"toolcall_delta\", contentIndex, delta: jsonDelta, partial: output });\n\n// Complete\nstream.push({\n  type: \"toolcall_end\",\n  contentIndex,\n  toolCall: { type: \"toolCall\", id, name, arguments: block.arguments },\n  partial: output\n});\n```\n\n### 사용량 및 비용\n\nAPI 응답의 사용량을 업데이트하고 비용을 계산합니다.\n\n```typescript\noutput.usage.input = response.usage.input_tokens;\noutput.usage.output = response.usage.output_tokens;\noutput.usage.cacheRead = response.usage.cache_read_tokens ?? 0;\noutput.usage.cacheWrite = response.usage.cache_write_tokens ?? 0;\noutput.usage.totalTokens = output.usage.input + output.usage.output +\n                           output.usage.cacheRead + output.usage.cacheWrite;\ncalculateCost(model, output.usage);\n```\n\n### 컨텍스트 오버플로 오류\n\n요청이 모델의 컨텍스트 창을 초과하면 pi는 대화를 압축하고 재시도하여 자동으로 복구할 수 있습니다. 이 복구는 pi가 실패를 오버플로로 인식하는 경우에만 시작됩니다.\n\n최종 어시스턴트 메시지에서 감지가 실행됩니다.\n\n- `stopReason === \"error\"`\n- `errorMessage`는 pi의 알려진 오버플로 패턴 중 하나와 일치합니다([`packages/ai/src/utils/overflow.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/utils/overflow.ts) 참조).\n\n공급자가 pi가 인식하지 못하는 메시지와 함께 오버플로 오류를 반환하는 경우 공급자를 등록하는 동일한 확장에서 오류를 정규화합니다. `message_end` 핸들러를 사용하여 보조 메시지를 다시 작성하면 `errorMessage`가 pi가 인식하는 문구로 시작됩니다. 일반적인 대체 `context_length_exceeded`가 가장 안전한 선택입니다.\n\n```typescript\nconst MY_PROVIDER_OVERFLOW_PATTERN = /your provider's overflow phrase/i;\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerProvider(\"my-provider\", { /* ... */ });\n\n  pi.on(\"message_end\", (event, ctx) => {\n    const message = event.message;\n    if (message.role !== \"assistant\") return;\n    if (message.stopReason !== \"error\") return;\n    if (\n      message.provider !== \"my-provider\" &&\n      ctx.model?.provider !== \"my-provider\"\n    )\n      return;\n\n    const errorMessage = message.errorMessage ?? \"\";\n    if (errorMessage.includes(\"context_length_exceeded\")) return;\n    if (!MY_PROVIDER_OVERFLOW_PATTERN.test(errorMessage)) return;\n\n    return {\n      message: {\n        ...message,\n        errorMessage: `context_length_exceeded: ${errorMessage}`,\n      },\n    };\n  });\n}\n```\n\n`message_end`는 pi가 자동 압축을 위한 보조 메시지를 추적하기 전에 실행되므로 다시 작성된 `errorMessage`가 pi가 확인하는 것입니다. 이를 적용하면 pi는 다음을 수행합니다.\n\n1. `errorMessage`에서 오버플로를 감지합니다.\n2. 라이브 컨텍스트에서 실패한 어시스턴트 메시지를 삭제하세요.\n3. 압축을 실행합니다.\n4. 요청을 한 번 다시 시도하세요.\n\n재작성 시 주의 깊게 보호하세요.\n\n- 범위를 제공업체(`message.provider` 및 `ctx.model?.provider`)로 지정하여 다른 제공업체의 관련 없는 오류가 수정되지 않도록 하세요.\n- pi의 일반적인 오버플로 패턴이 아닌 공급자별 패턴을 일치시킵니다. 속도 제한 또는 조절 오류(`rate limit`, `too many requests`)를 다시 작성하면 pi의 일반적인 백오프 재시도 경로 대신 압축이 잘못 트리거됩니다.\n- `errorMessage`에 이미 `context_length_exceeded`가 포함되어 있으면 건너뛰어 핸들러가 멱등성을 갖습니다.\n\n### 등록\n\n스트림 기능을 등록하세요.\n\n```typescript\npi.registerProvider(\"my-provider\", {\n  baseUrl: \"https://api.example.com\",\n  apiKey: \"$MY_API_KEY\",\n  api: \"my-custom-api\",\n  models: [...],\n  streamSimple: streamMyProvider\n});\n```\n\n## 구현 테스트\n\n기본 제공 공급자가 사용하는 것과 동일한 테스트 모음에 대해 공급자를 테스트합니다. [packages/ai/test/](https://github.com/earendil-works/pi-mono/tree/main/packages/ai/test)에서 다음 테스트 파일을 복사하고 조정하세요.\n\n| 시험 | 목적 |\n|------|---------|\n| `stream.test.ts` | 기본 스트리밍, 텍스트 출력 |\n| `tokens.test.ts` | 토큰 계산 및 사용 |\n| `abort.test.ts` | Abort신호 처리 |\n| `empty.test.ts` | 비어 있음/최소 응답 |\n| `context-overflow.test.ts` | 컨텍스트 창 제한 |\n| `image-limits.test.ts` | 이미지 입력 ​​처리 |\n| `unicode-surrogate.test.ts` | 유니코드 엣지 케이스 |\n| `tool-call-without-result.test.ts` | 도구 호출 엣지 케이스 |\n| `image-tool-result.test.ts` | 도구 결과의 이미지 |\n| `total-tokens.test.ts` | 총 토큰 계산 |\n| `cross-provider-handoff.test.ts` | 공급자 간 컨텍스트 핸드오프 |\n\n공급자/모델 쌍으로 테스트를 실행하여 호환성을 확인하세요.\n\n## 구성 참조\n\n```typescript\ninterface ProviderConfig {\n  /** Display name for the provider in UI such as /login. */\n  name?: string;\n\n  /** API endpoint URL. Required when defining models. */\n  baseUrl?: string;\n\n  /** API key literal, env interpolation ($ENV_VAR or ${ENV_VAR}), or !command. Required when defining models (unless oauth). */\n  apiKey?: string;\n\n  /** API type for streaming. Required at provider or model level when defining models. */\n  api?: Api;\n\n  /** Custom streaming implementation for non-standard APIs. */\n  streamSimple?: (\n    model: Model<Api>,\n    context: Context,\n    options?: SimpleStreamOptions\n  ) => AssistantMessageEventStream;\n\n  /** Custom headers to include in requests. Values use the same resolution syntax as apiKey. */\n  headers?: Record<string, string>;\n\n  /** If true, adds Authorization: Bearer header with the resolved API key. */\n  authHeader?: boolean;\n\n  /** Models to register. If provided, replaces all existing models for this provider. */\n  models?: ProviderModelConfig[];\n\n  /** OAuth provider for /login support. */\n  oauth?: {\n    name: string;\n    login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials>;\n    refreshToken(credentials: OAuthCredentials, signal: AbortSignal): Promise<OAuthCredentials>;\n    getApiKey(credentials: OAuthCredentials): string;\n  };\n}\n```\n\n## 모델 정의 참조\n\n```typescript\ninterface ProviderModelConfig {\n  /** Model ID (e.g., \"claude-sonnet-4-20250514\"). */\n  id: string;\n\n  /** Display name (e.g., \"Claude 4 Sonnet\"). */\n  name: string;\n\n  /** API type override for this specific model. */\n  api?: Api;\n\n  /** API endpoint URL override for this specific model. */\n  baseUrl?: string;\n\n  /** Whether the model supports extended thinking. */\n  reasoning: boolean;\n\n  /** Maps pi thinking levels to provider/model-specific values; null marks a level unsupported. */\n  thinkingLevelMap?: Partial<Record<\"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\", string | null>>;\n\n  /** Supported input types. */\n  input: (\"text\" | \"image\")[];\n\n  /** Cost per million tokens (for usage tracking). */\n  cost: {\n    input: number;\n    output: number;\n    cacheRead: number;\n    cacheWrite: number;\n  };\n\n  /** Maximum context window size in tokens. */\n  contextWindow: number;\n\n  /** Maximum output tokens. */\n  maxTokens: number;\n\n  /** Custom headers for this specific model. */\n  headers?: Record<string, string>;\n\n  /** Compatibility settings for the selected API. */\n  compat?: {\n    // openai-completions\n    supportsStore?: boolean;\n    supportsDeveloperRole?: boolean;\n    supportsReasoningEffort?: boolean;\n    supportsUsageInStreaming?: boolean;\n    supportsFinishReason?: boolean;\n    supportsStrictMode?: boolean;\n    supportsOpenAIGrammarTools?: boolean; // openai-completions/openai-responses; false falls back to normal function tools\n    maxTokensField?: \"max_completion_tokens\" | \"max_tokens\";\n    requiresToolResultName?: boolean;\n    requiresAssistantAfterToolResult?: boolean;\n    requiresThinkingAsText?: boolean;\n    requiresReasoningContentOnAssistantMessages?: boolean;\n    thinkingFormat?: \"openai\" | \"openrouter\" | \"deepseek\" | \"together\" | \"baseten\" | \"zai\" | \"qwen\" | \"chat-template\" | \"qwen-chat-template\" | \"string-thinking\" | \"ant-ling\";\n    chatTemplateKwargs?: Record<string, string | number | boolean | null | { \"$var\": \"thinking.enabled\" | \"thinking.effort\"; omitWhenOff?: boolean }>;\n    chatTemplateArgs?: Record<string, string | number | boolean | null | { \"$var\": \"thinking.enabled\" | \"thinking.effort\"; omitWhenOff?: boolean }>;\n    cacheControlFormat?: \"anthropic\";\n    sessionAffinityFormat?: \"openai\" | \"openai-nosession\" | \"openrouter\";\n    sendSessionAffinityHeaders?: boolean;\n\n    // anthropic-messages\n    supportsEagerToolInputStreaming?: boolean;\n    supportsLongCacheRetention?: boolean;\n    sendSessionAffinityHeaders?: boolean;\n    supportsCacheControlOnTools?: boolean;\n    forceAdaptiveThinking?: boolean;\n    allowEmptySignature?: boolean;\n    supportsStrictTools?: boolean;\n  };\n}\n```\n\n`openrouter`가 `reasoning: { effort }`을 보냅니다. `deepseek`는 활성화되면 `thinking: { type: \"enabled\" | \"disabled\" }` 및 `reasoning_effort`를 보냅니다. `together`는 `reasoning: { enabled }`를 전송하고 `supportsReasoningEffort`가 활성화되면 `reasoning_effort`도 전송합니다. `qwen`는 DashScope 스타일 최상위 `enable_thinking`용입니다. `chat_template_kwargs.enable_thinking`를 읽고 `preserve_thinking`이 필요한 로컬 Qwen 호환 서버에는 `qwen-chat-template`를 사용하세요. `chat_template_kwargs`를 구성하려면 `chat-template`를 사용하세요. 예를 들어 `chatTemplateKwargs: { \"thinking\": { \"$var\": \"thinking.enabled\" } }`이 있는 vLLM 뒤의 DeepSeek V3.x입니다. 공급자가 `chat_template_args` 아래의 토글 값을 예상하고 선택적으로 최상위 `reasoning_effort`를 지원하는 경우 `thinkingFormat: \"baseten\"`를 `chatTemplateArgs`와 함께 사용하세요.\n`cacheControlFormat: \"anthropic\"`는 인류 스타일 `cache_control` 마커를 시스템 프롬프트, 마지막 도구 정의, 마지막 사용자, 보조자 또는 도구 결과 텍스트 콘텐츠에 적용합니다.","sourceFile":"custom-provider.md"},"development":{"title":"개발","markdown":"추가 지침은 [AGENTS.md](https://github.com/earendil-works/pi-mono/blob/main/AGENTS.md)를 참조하세요.\n\n## 설정\n\n```bash\ngit clone https://github.com/earendil-works/pi-mono\ncd pi-mono\nnpm install\nnpm run build\n```\n\n소스에서 실행:\n\n```bash\n/path/to/pi-mono/pi-test.sh\n```\n\n스크립트는 모든 디렉터리에서 실행될 수 있습니다. Pi 호출자의 현재 작업 디렉터리를 유지합니다.\n\n## 포크/리브랜딩\n\n`package.json`를 통해 구성:\n\n```json\n{\n  \"piConfig\": {\n    \"name\": \"pi\",\n    \"configDir\": \".pi\"\n  }\n}\n```\n\n포크의 `name`, `configDir` 및 `bin` 필드를 변경합니다. CLI 배너, 구성 경로 및 환경 변수 이름에 영향을 줍니다.\n\n## 경로 확인\n\n세 가지 실행 모드: npm 설치, 독립형 바이너리, 소스의 tsx.\n\n**패키지 자산에는 항상 `src/config.ts`**를 사용하세요.\n\n```typescript\nimport { getPackageDir, getThemeDir } from \"./config.js\";\n```\n\n패키지 자산에 `__dirname`를 직접 사용하지 마십시오.\n\n## 디버그 명령\n\n`/debug`(숨김)이 `~/.pi/agent/pi-debug.log`에 씁니다:\n- ANSI 코드로 렌더링된 TUI 라인\n- LLM에 보낸 마지막 메시지\n\n## 테스트\n\n```bash\n./test.sh                         # Run non-LLM tests (no API keys needed)\nnpm test                          # Run all tests\nnpm test -- test/specific.test.ts # Run specific test\n```\n\n## 프로젝트 구조\n\n```\npackages/\n  ai/           # LLM provider abstraction\n  agent/        # Agent loop and message types  \n  tui/          # Terminal UI components\n  coding-agent/ # CLI and interactive mode\n```","sourceFile":"development.md"},"environment-variables":{"title":"환경 변수","markdown":"Pi는 세 가지 방법으로 환경 변수를 사용합니다.\n\n- `PI_OFFLINE`와 같은 변수는 Pi 프로세스를 구성합니다.\n- Pi는 `PI_CODING_AGENT`를 설정하여 하위 프로세스가 Pi 내부에서 실행되는지 감지할 수 있습니다.\n- LLM 호출 가능 bash 도구에 의해 실행되는 명령은 현재 세션을 설명하는 `PI_*` 변수를 수신합니다.\n\n공급자 API-주요 변수는 [Providers](providers.md#environment-variables-or-auth-file)에 별도로 문서화되어 있습니다.\n\n## 프로세스 마커\n\nCLI 및 RPC 진입점은 `PI_CODING_AGENT=true`로 설정됩니다. 하위 프로세스는 이를 상속하고 이를 사용하여 Pi 내부에서 실행되는지 감지할 수 있습니다. 세션별로 지정되지 않으며 SDK를 통해 Pi가 삽입되면 자동으로 설정되지 않습니다.\n\n## Bash 도구 세션 환경\n\nbash 도구로 실행되는 명령은 현재 Pi 세션 상태를 수신합니다.\n\n| 변하기 쉬운 | 설명 |\n|----------|-------------|\n| `PI_SESSION_ID` | 현재 세션 ID |\n| `PI_SESSION_FILE` | 현재 세션 JSONL 파일의 절대 경로입니다. 임시 세션이 설정되지 않았습니다. |\n| `PI_PROVIDER` | 현재 선택된 모델 제공자 |\n| `PI_MODEL` | 현재 선택된 모델 ID |\n| `PI_REASONING_LEVEL` | 현재 유효 추론 수준: `off`, `minimal`, `low`, `medium`, `high`, `xhigh` 또는 `max` |\n\n각 명령이 시작될 때 값이 확인됩니다. 따라서 모델을 전환하거나 추론 수준을 변경하면 Pi을 다시 시작하지 않고도 다음 bash 명령에 영향을 미칩니다. `PI_PROVIDER` 및 `PI_MODEL`는 선택된 Pi 모델을 식별하며, 라우터가 내부적으로 선택할 수 있는 다른 업스트림 모델이 아닙니다.\n\n어떤 모델이나 공급자가 실행 중인지 묻는 메시지가 나타나면 시스템 프롬프트에서 답변을 추론하는 대신 다음 변수를 검사하세요.\n\n```bash\nprintf '%s/%s\\n' \"$PI_PROVIDER\" \"$PI_MODEL\"\nprintf 'reasoning=%s session=%s\\n' \"$PI_REASONING_LEVEL\" \"$PI_SESSION_ID\"\n```\n\n세션이 지속되면 세션 파일을 직접 검사할 수 있습니다.\n\n```bash\nif [ -n \"$PI_SESSION_FILE\" ]; then\n  tail -n 1 \"$PI_SESSION_FILE\"\nfi\n```\n\n이러한 변수는 LLM 호출 가능 bash 도구에 주입됩니다. 사용자가 입력한 `!` 또는 `!!` 명령에는 삽입되지 않습니다.\n\n### 사용자 정의 Bash 도구\n\n`createBashTool()`로 생성된 Bash 도구는 Pi로 등록할 때 기본적으로 세션 환경을 노출합니다. 주입은 `spawnHook` 이전에 발생하므로 후크는 `ctx.env`에서 변수를 받습니다.\n\n```typescript\nconst bashTool = createBashTool(cwd, {\n  spawnHook: (ctx) => ({\n    ...ctx,\n    env: { ...ctx.env, CI: \"1\" },\n  }),\n});\n```\n\n생성 후크와 별도로 세션 메타데이터를 비활성화합니다.\n\n```typescript\nconst bashTool = createBashTool(cwd, {\n  exposeSessionEnvironment: false,\n  spawnHook: (ctx) => ctx,\n});\n```\n\n비활성화되면 Pi는 이러한 변수에 대해 상속된 값을 제거하므로 중첩된 Pi 프로세스는 오래된 상위 세션 메타데이터를 노출하지 않습니다.\n\n## Pi 프로세스 구성\n\n다음 변수는 Pi 자체에서 읽습니다.\n\n| 변하기 쉬운 | 설명 |\n|----------|-------------|\n| `PI_CODING_AGENT_DIR` | 구성 디렉터리를 재정의합니다. 기본값은 `~/.pi/agent`입니다. |\n| `PI_CODING_AGENT_SESSION_DIR` | 세션 저장소를 재정의합니다. `--session-dir`에 의해 재정의됨 |\n| `PI_PACKAGE_DIR` | Nix/Guix 저장소 경로에 유용한 패키지 디렉터리를 재정의합니다. |\n| `PI_OFFLINE` | 업데이트 확인, 패키지 업데이트, 원격 측정 설치/업데이트 등 시작 네트워크 작업을 비활성화합니다. |\n| `PI_SKIP_VERSION_CHECK` | `pi.dev` 최신 버전 요청 비활성화 |\n| `PI_TELEMETRY` | 설치/업데이트 원격 측정 및 공급자 속성 헤더 재정의: `1`/`true`/`yes` 또는 `0`/`false`/`no` |\n| `PI_CACHE_RETENTION` | 지원되는 경우 확장된 공급자 프롬프트 캐싱을 위해 `long`로 설정 |\n| `PI_SHARE_VIEWER_URL` | `/share`에서 사용하는 기본 URL을 재정의합니다. |\n| `PI_HARDWARE_CURSOR` | 하드웨어 커서를 표시하려면 `1`로 설정하세요. [Terminal setup](terminal-setup.md) 참조 |\n| `VISUAL`, `EDITOR` | `externalEditor`가 설정되지 않은 경우 외부 편집기 대체 |\n| `HTTP_PROXY`, `HTTPS_PROXY` | 프록시 아웃바운드 HTTP 요청 |\n\n`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`와 같은 공급자 자격 증명 및 클라우드 공급자 구성은 [Providers](providers.md#environment-variables-or-auth-file)에 나열됩니다.","sourceFile":"environment-variables.md"},"extensions":{"title":"Extensions","markdown":"> pi는 확장을 만들 수 있습니다. 귀하의 사용 사례에 맞게 구축하도록 요청하세요.\n\n\nExtensions는 pi의 동작을 확장하는 TypeScript 모듈입니다. 수명 주기 이벤트를 구독하고, LLM에서 호출할 수 있는 사용자 정의 도구를 등록하고, 명령을 추가하는 등의 작업을 수행할 수 있습니다.\n\n> **/reload 배치:** 자동 검색을 위해 `~/.pi/agent/extensions/`(전역) 또는 `.pi/extensions/`(프로젝트-로컬)에 확장을 넣습니다. 빠른 테스트에만 `pi -e./path.ts`를 사용하세요. Extensions 자동 검색된 위치는 `/reload`로 핫 리로드될 수 있습니다.\n\n**주요 기능:**\n- **사용자 정의 도구** - LLM이 `pi.registerTool()`를 통해 호출할 수 있는 도구를 등록합니다.\n- **이벤트 차단** - 도구 호출 차단 또는 수정, 컨텍스트 삽입, 압축 사용자 정의\n- **사용자 상호작용** - `ctx.ui`을 통해 사용자에게 메시지 표시(선택, 확인, 입력, 알림)\n- **사용자 정의 UI 구성 요소** - 복잡한 상호 작용을 위해 `ctx.ui.custom()`를 통한 키보드 입력이 포함된 전체 TUI 구성 요소\n- **사용자 정의 명령** - `pi.registerCommand()`을 통해 `/mycommand`와 같은 명령을 등록합니다.\n- **세션 지속성** - `pi.appendEntry()`을 통해 다시 시작해도 유지되는 상태 저장\n- **맞춤 렌더링** - 도구 호출/결과 및 메시지가 TUI에 표시되는 방식 제어\n\n**사용 사례 예시:**\n- 허가 게이트(`rm -rf`, `sudo` 등 이전에 확인)\n- Git 체크포인트(매 턴마다 보관, 분기에 복원)\n- 경로 보호(`.env`, `node_modules/`에 대한 쓰기 차단)\n- 사용자 정의 압축(대화를 원하는 방식으로 요약)\n- 대화 요약(`summarize.ts` 예 참조)\n- 대화형 도구(질문, 마법사, 사용자 정의 대화 상자)\n- 상태 저장 도구(할 일 목록, 연결 풀)\n- 외부 통합(파일 감시자, 웹후크, CI 트리거)\n- 기다리는 동안 게임을 즐기세요(`snake.ts` 예시 참조)\n\n작업 구현은 [examples/extensions/](../examples/extensions/)를 참조하세요.\n\n## 목차\n\n- [Quick Start](#quick-start)\n- [Extension Locations](#extension-locations)\n- [Available Imports](#available-imports)\n- [Writing an Extension](#writing-an-extension)\n  - [Extension Styles](#extension-styles)\n- [Events](#events)\n  - [Lifecycle Overview](#lifecycle-overview)\n  - [Resource Events](#resource-events)\n  - [Session Events](#session-events)\n  - [Agent Events](#agent-events)\n  - [Model Events](#model-events)\n  - [Tool Events](#tool-events)\n- [ExtensionContext](#extensioncontext)\n- [ExtensionCommandContext](#extensioncommandcontext)\n- [ExtensionAPI Methods](#extensionapi-methods)\n- [State Management](#state-management)\n- [Custom Tools](#custom-tools)\n  - [Dynamic Tool Loading](#dynamic-tool-loading)\n- [Custom UI](#custom-ui)\n- [Error Handling](#error-handling)\n- [Mode Behavior](#mode-behavior)\n- [Examples Reference](#examples-reference)\n\n## 빠른 시작\n\n`~/.pi/agent/extensions/my-extension.ts` 만들기:\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { Type } from \"typebox\";\n\nexport default function (pi: ExtensionAPI) {\n  // React to events\n  pi.on(\"session_start\", async (_event, ctx) => {\n    ctx.ui.notify(\"Extension loaded!\", \"info\");\n  });\n\n  pi.on(\"tool_call\", async (event, ctx) => {\n    if (event.toolName === \"bash\" && event.input.command?.includes(\"rm -rf\")) {\n      const ok = await ctx.ui.confirm(\"Dangerous!\", \"Allow rm -rf?\");\n      if (!ok) return { block: true, reason: \"Blocked by user\" };\n    }\n  });\n\n  // Register a custom tool\n  pi.registerTool({\n    name: \"greet\",\n    label: \"Greet\",\n    description: \"Greet someone by name\",\n    parameters: Type.Object({\n      name: Type.String({ description: \"Name to greet\" }),\n    }),\n    async execute(toolCallId, params, signal, onUpdate, ctx) {\n      return {\n        content: [{ type: \"text\", text: `Hello, ${params.name}!` }],\n        details: {},\n      };\n    },\n  });\n\n  // Register a command\n  pi.registerCommand(\"hello\", {\n    description: \"Say hello\",\n    handler: async (args, ctx) => {\n      ctx.ui.notify(`Hello ${args || \"world\"}!`, \"info\");\n    },\n  });\n}\n```\n\n`--extension`(또는 `-e`) 플래그로 테스트합니다.\n\n```bash\npi -e ./my-extension.ts\n```\n\n## 확장 위치\n\n> **보안:** Extensions 전체 시스템 권한으로 실행하고 임의 코드를 실행할 수 있습니다. 신뢰할 수 있는 소스에서만 설치하세요.\n\nExtensions은 신뢰할 수 있는 위치에서 자동으로 검색됩니다. Project-local `.pi/extensions` 항목은 프로젝트를 신뢰한 후에만 로드됩니다.\n\n| 위치 | 범위 |\n|----------|-------|\n| `~/.pi/agent/extensions/*.ts` | 글로벌(모든 프로젝트) |\n| `~/.pi/agent/extensions/*/index.ts` | 전역(하위 디렉터리) |\n| `.pi/extensions/*.ts` | 프로젝트-로컬 |\n| `.pi/extensions/*/index.ts` | 프로젝트-로컬(하위 디렉터리) |\n\n`settings.json`를 통한 추가 경로:\n\n```json\n{\n  \"packages\": [\n    \"npm:@foo/bar@1.0.0\",\n    \"git:github.com/user/repo@v1\"\n  ],\n  \"extensions\": [\n    \"/path/to/local/extension.ts\",\n    \"/path/to/local/extension/dir\"\n  ]\n}\n```\n\nnpm 또는 git을 pi 패키지로 공유하려면 [packages.md](packages.md)를 참조하세요.\n\n## 사용 가능한 수입품\n\n| 패키지 | 목적 |\n|---------|---------|\n| `@earendil-works/pi-coding-agent` | 확장 유형(`ExtensionAPI`, `ExtensionContext`, 이벤트) |\n| `typebox` | 도구 매개변수에 대한 스키마 정의 |\n| `@earendil-works/pi-ai` | AI 유틸리티(Google 호환 열거형의 경우 `StringEnum`) |\n| `@earendil-works/pi-tui` | TUI 맞춤 렌더링을 위한 구성요소 |\n\nnpm 종속성도 작동합니다. 확장 프로그램 옆(또는 상위 디렉토리)에 `package.json`를 추가하고 `npm install`를 실행하면 `node_modules/`에서의 가져오기가 자동으로 해결됩니다.\n\n`pi install`(npm 또는 git)로 설치된 분산 pi 패키지의 경우 런타임 deps는 `dependencies`에 있어야 합니다. 패키지 설치는 기본적으로 프로덕션 설치(`npm install --omit=dev`)를 사용하므로 런타임 시 `devDependencies`를 사용할 수 없습니다. `npmCommand`가 구성되면 git 패키지는 래퍼와의 호환성을 위해 일반 `install`를 사용합니다.\n\nNode.js 내장 기능(`node:fs`, `node:path` 등)도 사용할 수 있습니다.\n\n## 확장 작성\n\n확장은 `ExtensionAPI`를 수신하는 기본 팩토리 함수를 내보냅니다. 팩토리는 동기식 또는 비동기식일 수 있습니다.\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default function (pi: ExtensionAPI) {\n  // Subscribe to events\n  pi.on(\"event_name\", async (event, ctx) => {\n    // ctx.ui for user interaction\n    const ok = await ctx.ui.confirm(\"Title\", \"Are you sure?\");\n    ctx.ui.notify(\"Done!\", \"info\");\n    ctx.ui.setStatus(\"my-ext\", \"Processing...\");  // Footer status\n    ctx.ui.setWidget(\"my-ext\", [\"Line 1\", \"Line 2\"]);  // Widget above editor (default)\n  });\n\n  // Register tools, commands, shortcuts, flags\n  pi.registerTool({ ... });\n  pi.registerCommand(\"name\", { ... });\n  pi.registerShortcut(\"ctrl+x\", { ... });\n  pi.registerFlag(\"my-flag\", { ... });\n}\n```\n\nExtensions은 [jiti](https://github.com/unjs/jiti)를 통해 로드되므로 TypeScript는 컴파일 없이 작동합니다.\n\n공장에서 `Promise`를 반환하면 pi는 시작을 계속하기 전에 이를 기다립니다. 이는 `session_start` 이전, `resources_discover` 이전 및 `pi.registerProvider()`을 통해 대기열에 있는 공급자 등록이 플러시되기 전에 비동기 초기화가 완료됨을 의미합니다.\n\n### 비동기 팩토리 기능\n\n원격 구성을 가져오거나 사용 가능한 모델을 동적으로 검색하는 등의 일회성 시작 작업에 비동기 팩토리를 사용하세요.\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default async function (pi: ExtensionAPI) {\n  const response = await fetch(\"http://localhost:1234/v1/models\");\n  const payload = (await response.json()) as {\n    data: Array<{\n      id: string;\n      name?: string;\n      context_window?: number;\n      max_tokens?: number;\n    }>;\n  };\n\n  pi.registerProvider(\"local-openai\", {\n    baseUrl: \"http://localhost:1234/v1\",\n    apiKey: \"$LOCAL_OPENAI_API_KEY\",\n    api: \"openai-completions\",\n    models: payload.data.map((model) => ({\n      id: model.id,\n      name: model.name ?? model.id,\n      reasoning: false,\n      input: [\"text\"],\n      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n      contextWindow: model.context_window ?? 128000,\n      maxTokens: model.max_tokens ?? 4096,\n    })),\n  });\n}\n```\n\n이 패턴을 사용하면 일반 시작 및 `pi --list-models`에서 가져온 모델을 사용할 수 있습니다.\n\n### 수명이 긴 리소스 및 종료\n\n확장 팩토리는 세션을 시작하지 않는 호출로 실행될 수 있습니다. 공장에서 프로세스, 소켓, 파일 감시자 또는 타이머와 같은 백그라운드 리소스를 시작하지 마십시오.\n\n`session_start` 또는 리소스가 필요한 명령/도구/이벤트가 발생할 때까지 백그라운드 리소스 시작을 연기합니다. 시작한 모든 세션 범위 리소스를 닫으려면 멱등성 `session_shutdown` 핸들러를 등록하세요.\n\n### 확장 스타일\n\n**단일 파일** - 소규모 확장의 경우 가장 간단함:\n\n```\n~/.pi/agent/extensions/\n└── my-extension.ts\n```\n\n**index.ts가 있는 디렉터리** - 다중 파일 확장자의 경우:\n\n```\n~/.pi/agent/extensions/\n└── my-extension/\n    ├── index.ts        # Entry point (exports default function)\n    ├── tools.ts        # Helper module\n    └── utils.ts        # Helper module\n```\n\n**종속성이 있는 패키지** - npm 패키지가 필요한 확장 프로그램의 경우:\n\n```\n~/.pi/agent/extensions/\n└── my-extension/\n    ├── package.json    # Declares dependencies and entry points\n    ├── package-lock.json\n    ├── node_modules/   # After npm install\n    └── src/\n        └── index.ts\n```\n\n```json\n// package.json\n{\n  \"name\": \"my-extension\",\n  \"dependencies\": {\n    \"zod\": \"^3.0.0\",\n    \"chalk\": \"^5.0.0\"\n  },\n  \"pi\": {\n    \"extensions\": [\"./src/index.ts\"]\n  }\n}\n```\n\n확장 디렉토리에서 `npm install`를 실행하면 `node_modules/`에서 가져오기가 자동으로 작동합니다.\n\n## 이벤트\n\n### 수명주기 개요\n\n```\npi starts\n  │\n  ├─► project_trust (user/global and CLI extensions only, before project resources load)\n  ├─► session_start { reason: \"startup\" }\n  └─► resources_discover { reason: \"startup\" }\n      │\n      ▼\nuser sends prompt ─────────────────────────────────────────┐\n  │                                                        │\n  ├─► (extension commands checked first, bypass if found)  │\n  ├─► input (can intercept, transform, or handle)          │\n  ├─► (skill/template expansion if not handled)            │\n  ├─► before_agent_start (can inject message, modify system prompt)\n  ├─► agent_start                                          │\n  ├─► message_start / message_update / message_end         │\n  │                                                        │\n  │   ┌─── turn (repeats while LLM calls tools) ───┐       │\n  │   │                                            │       │\n  │   ├─► turn_start                               │       │\n  │   ├─► context (can modify messages)            │       │\n  │   ├─► before_provider_headers (can mutate headers)     |\n  │   ├─► before_provider_request (can inspect or replace payload)\n  │   ├─► after_provider_response (status + headers, before stream consume)\n  │   │                                            │       │\n  │   │   LLM responds, may call tools:            │       │\n  │   │     ├─► tool_execution_start               │       │\n  │   │     ├─► tool_call (can block)              │       │\n  │   │     ├─► tool_execution_update              │       │\n  │   │     ├─► tool_result (can modify)           │       │\n  │   │     └─► tool_execution_end                 │       │\n  │   │                                            │       │\n  │   └─► turn_end                                 │       │\n  │                                                        │\n  ├─► agent_end                                            │\n  └─► agent_settled (no retry/compaction/follow-up left)   │\n                                                           │\nuser sends another prompt ◄────────────────────────────────┘\n\n/new (new session) or /resume (switch session)\n  ├─► session_before_switch (can cancel)\n  ├─► session_shutdown\n  ├─► session_start { reason: \"new\" | \"resume\", previousSessionFile? }\n  └─► resources_discover { reason: \"startup\" }\n\n/fork or /clone\n  ├─► session_before_fork (can cancel)\n  ├─► session_shutdown\n  ├─► session_start { reason: \"fork\", previousSessionFile }\n  └─► resources_discover { reason: \"startup\" }\n\n/name or pi.setSessionName()\n  └─► session_info_changed\n\n/compact or auto-compaction\n  ├─► session_before_compact (can cancel or customize)\n  └─► session_compact\n\n/tree navigation\n  ├─► session_before_tree (can cancel or customize)\n  └─► session_tree\n\n/model or Ctrl+P (model selection/cycling)\n  ├─► thinking_level_select (if model change changes/clamps thinking level)\n  └─► model_select\n\nthinking level changes (settings, keybinding, pi.setThinkingLevel())\n  └─► thinking_level_select\n\nexit (Ctrl+C, Ctrl+D, SIGHUP, SIGTERM)\n  └─► session_shutdown\n```\n\n### 스타트업 이벤트\n\n#### 프로젝트_신뢰\n\npi가 동적 구성(`.pi` 또는 `.agents/skills`)이 포함된 프로젝트를 신뢰할지 여부를 결정하기 전에 실행됩니다. 시작하는 동안 그리고 세션 교체(예: `/resume`)가 현재 프로세스에서 신뢰가 확인되지 않은 cwd에 들어갈 때 실행됩니다. 사용자/전역 확장 및 CLI `-e` 확장만 참여합니다. 프로젝트 로컬 확장은 신뢰가 해결될 때까지 로드되지 않습니다.\n\n```typescript\npi.on(\"project_trust\", async (event, ctx) => {\n  // event.cwd - current working directory\n  // ctx has a limited trust context: cwd, mode, hasUI, and select/confirm/input/notify UI helpers\n  if (await ctx.ui.confirm(\"Trust project?\", event.cwd)) {\n    return { trusted: \"yes\", remember: true };\n  }\n  return { trusted: \"undecided\" };\n});\n```\n\n`project_trust` 핸들러는 `{ trusted: \"yes\" | \"no\" | \"undecided\" }`를 반환해야 합니다. `\"yes\"` 또는 `\"no\"`를 반환하는 사용자/전역 또는 CLI 확장이 결정을 소유합니다. 첫 번째 예/아니요 결정이 승리하고 기본 제공 신뢰 프롬프트가 표시되지 않습니다. 예/아니요 결정을 유지하려면 `remember: true`를 사용하세요. 그렇지 않으면 현재 프로세스에만 적용됩니다. 이후 핸들러나 내장된 신뢰 흐름이 결정하도록 하려면 `\"undecided\"`를 반환합니다. 메시지를 표시하기 전에 `ctx.hasUI`를 확인하세요. 핸들러가 예/아니요를 반환하지 않으면 일반적인 신뢰 해결이 계속됩니다. 저장된 `trust.json` 결정이 먼저 적용된 다음 `defaultProjectTrust`는 기본적으로 pi의 요청, 신뢰 또는 거부 여부를 제어합니다.\n\n### 자원 이벤트\n\n#### 리소스_발견\n\n확장 프로그램이 추가 기술, 프롬프트 및 테마 경로에 기여할 수 있도록 `session_start` 이후에 실행됩니다.\n시작 경로는 `reason: \"startup\"`를 사용합니다. 다시 로드는 `reason: \"reload\"`를 사용합니다.\n\n```typescript\npi.on(\"resources_discover\", async (event, _ctx) => {\n  // event.cwd - current working directory\n  // event.reason - \"startup\" | \"reload\"\n  return {\n    skillPaths: [\"/path/to/skills\"],\n    promptPaths: [\"/path/to/prompts\"],\n    themePaths: [\"/path/to/themes\"],\n  };\n});\n```\n\n### 세션 이벤트\n\n세션 저장소 내부 및 SessionManager API에 대해서는 [Session Format](session-format.md)를 참조하세요.\n\n#### 세션_시작\n\n세션이 시작, 로드 또는 다시 로드될 때 발생합니다.\n\n```typescript\npi.on(\"session_start\", async (event, ctx) => {\n  // event.reason - \"startup\" | \"reload\" | \"new\" | \"resume\" | \"fork\"\n  // event.previousSessionFile - present for \"new\", \"resume\", and \"fork\"\n  ctx.ui.notify(`Session: ${ctx.sessionManager.getSessionFile() ?? \"ephemeral\"}`, \"info\");\n});\n```\n\n#### 세션_정보_변경됨\n\n현재 세션 표시 이름이 `/name`, RPC 또는 `pi.setSessionName()`를 통해 설정되면 시작됩니다.\n\n```typescript\npi.on(\"session_info_changed\", async (event, ctx) => {\n  // event.name - current normalized name, or undefined if cleared\n  ctx.ui.notify(`Session renamed: ${event.name ?? \"(none)\"}`, \"info\");\n});\n```\n\n#### session_before_switch\n\n새 세션을 시작(`/new`)하거나 세션을 전환(`/resume`)하기 전에 실행됩니다.\n\n```typescript\npi.on(\"session_before_switch\", async (event, ctx) => {\n  // event.reason - \"new\" or \"resume\"\n  // event.targetSessionFile - session we're switching to (only for \"resume\")\n\n  if (event.reason === \"new\") {\n    const ok = await ctx.ui.confirm(\"Clear?\", \"Delete all messages?\");\n    if (!ok) return { cancel: true };\n  }\n});\n```\n\n성공적인 전환 또는 새 세션 작업 후 pi는 이전 확장 인스턴스에 대해 `session_shutdown`를 내보내고 새 세션에 대한 확장을 다시 로드하고 리바인드한 다음 `reason: \"new\" | \"resume\"` 및 `previousSessionFile`와 함께 `session_start`을 내보냅니다.\n`session_shutdown`에서 정리 작업을 수행한 다음 `session_start`에서 메모리 내 상태를 다시 설정합니다.\n\n#### session_before_fork\n\n`/fork`를 통해 분기하거나 `/clone`를 통해 복제할 때 실행됩니다.\n\n```typescript\npi.on(\"session_before_fork\", async (event, ctx) => {\n  // event.entryId - ID of the selected entry\n  // event.position - \"before\" for /fork, \"at\" for /clone\n  return { cancel: true }; // Cancel fork/clone\n  // OR\n  return { skipConversationRestore: true }; // Reserved for future conversation restore control\n});\n```\n\n성공적인 포크 또는 복제 후 pi는 이전 확장 인스턴스에 대해 `session_shutdown`를 내보내고 새 세션에 대한 확장을 다시 로드하고 리바인드한 다음 `reason: \"fork\"` 및 `previousSessionFile`를 사용하여 `session_start`을 내보냅니다.\n`session_shutdown`에서 정리 작업을 수행한 다음 `session_start`에서 메모리 내 상태를 다시 설정합니다.\n\n#### session_before_compact / 세션_콤팩트\n\n압축시 발사됩니다. 자세한 내용은 [compaction.md](compaction.md)를 참조하세요.\n\n```typescript\npi.on(\"session_before_compact\", async (event, ctx) => {\n  const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event;\n\n  // reason - \"manual\" (/compact), \"threshold\", or \"overflow\"\n  // willRetry - whether the aborted turn is retried after compaction (overflow recovery)\n\n  // Cancel:\n  return { cancel: true };\n\n  // Custom summary:\n  return {\n    compaction: {\n      summary: \"...\",\n      firstKeptEntryId: preparation.firstKeptEntryId,\n      tokensBefore: preparation.tokensBefore,\n      // usage: summaryResponse.usage, // Optional; included in session totals\n    }\n  };\n});\n\npi.on(\"session_compact\", async (event, ctx) => {\n  // event.compactionEntry - the saved compaction\n  // event.fromExtension - whether extension provided it\n  // event.reason - \"manual\" (/compact), \"threshold\", or \"overflow\"\n  // event.willRetry - whether the aborted turn is retried after compaction (overflow recovery)\n});\n```\n\n#### session_before_tree / 세션_트리\n\n`/tree` 탐색에서 실행됩니다. 트리 탐색 개념은 [Sessions](sessions.md)를 참조하세요.\n\n```typescript\npi.on(\"session_before_tree\", async (event, ctx) => {\n  const { preparation, signal } = event;\n  return { cancel: true };\n  // OR provide custom summary:\n  return {\n    summary: {\n      summary: \"...\",\n      // usage: summaryResponse.usage, // Optional; included in session totals\n      details: {},\n    },\n  };\n});\n\npi.on(\"session_tree\", async (event, ctx) => {\n  // event.newLeafId, oldLeafId, summaryEntry, fromExtension\n});\n```\n\n#### 세션 종료\n\n시작된 세션 런타임이 종료되기 전에 실행됩니다. `session_start` 또는 기타 세션 범위 후크에서 열린 리소스를 정리하려면 이를 사용합니다.\n\n```typescript\npi.on(\"session_shutdown\", async (event, ctx) => {\n  // event.reason - \"quit\" | \"reload\" | \"new\" | \"resume\" | \"fork\"\n  // event.targetSessionFile - destination session for session replacement flows\n  // Cleanup, save state, etc.\n});\n```\n\n### 에이전트 이벤트\n\n#### before_agent_start\n\n사용자가 프롬프트를 제출한 후 에이전트 루프 전에 실행됩니다. 메시지를 삽입하거나 시스템 프롬프트를 수정할 수 있습니다.\n\n```typescript\npi.on(\"before_agent_start\", async (event, ctx) => {\n  // event.prompt - user's prompt text\n  // event.images - attached images (if any)\n  // event.systemPrompt - current chained system prompt for this handler\n  //   (includes changes from earlier before_agent_start handlers)\n  // event.systemPromptOptions - structured options used to build the system prompt\n  //   .customPrompt - any custom system prompt (from --system-prompt, SYSTEM.md, or custom templates)\n  //   .selectedTools - tools currently active in the prompt\n  //   .toolSnippets - one-line descriptions for each tool\n  //   .promptGuidelines - custom guideline bullets\n  //   .appendSystemPrompt - text from --append-system-prompt flags\n  //   .cwd - working directory\n  //   .contextFiles - AGENTS.md files and other loaded context files\n  //   .skills - loaded skills\n\n  return {\n    // Inject a persistent message (stored in session, sent to LLM)\n    message: {\n      customType: \"my-extension\",\n      content: \"Additional context for the LLM\",\n      display: true,\n    },\n    // Replace the system prompt for this turn (chained across extensions)\n    systemPrompt: event.systemPrompt + \"\\n\\nExtra instructions for this turn...\",\n  };\n});\n```\n\n`systemPromptOptions` 필드는 Pi가 시스템 프롬프트를 구축하는 데 사용하는 것과 동일한 구조화된 데이터에 대한 액세스를 확장 프로그램에 제공합니다. 이를 통해 리소스를 다시 검색하거나 플래그를 다시 구문 분석하지 않고도 Pi가 로드한 내용(사용자 정의 프롬프트, 지침, 도구 조각, context files, 기술)을 검사할 수 있습니다. 확장 프로그램이 사용자가 제공한 구성을 존중하면서 시스템 프롬프트에 대해 심층적이고 정보에 입각한 변경을 수행해야 하는 경우 이를 사용하세요.\n\n`before_agent_start`, `event.systemPrompt` 및 `ctx.getSystemPrompt()` 내부에는 현재 핸들러의 연결된 시스템 프롬프트가 반영됩니다. 나중에 `before_agent_start` 핸들러가 이를 다시 수정할 수 있습니다.\n\n#### 에이전트_시작/에이전트_끝/에이전트_정착\n\n`agent_start` 낮은 수준의 에이전트 실행이 시작되면 실행됩니다. `agent_end`는 실행이 끝나면 실행되지만 Pi는 여전히 자동 재시도, 자동 압축 및 재시도하거나 대기 중인 후속 메시지를 계속 사용할 수 있습니다. Pi가 자동으로 계속 실행되지 않는다는 것을 알아야 하는 상태 통합의 경우 `agent_settled`를 사용하세요.\n\n```typescript\npi.on(\"agent_start\", async (_event, ctx) => {});\n\npi.on(\"agent_end\", async (event, ctx) => {\n  // event.messages - messages from this low-level run\n});\n\npi.on(\"agent_settled\", async (_event, ctx) => {\n  // ctx.isIdle() is true here unless another extension started a new run.\n});\n```\n\n#### 턴_시작 / 턴_엔드\n\n매 턴마다 실행됩니다(1개의 LLM 응답 + 도구 호출).\n\n```typescript\npi.on(\"turn_start\", async (event, ctx) => {\n  // event.turnIndex, event.timestamp\n});\n\npi.on(\"turn_end\", async (event, ctx) => {\n  // event.turnIndex, event.message, event.toolResults\n});\n```\n\n#### message_start / message_update / message_end\n\n메시지 수명 주기 업데이트를 위해 시작됩니다.\n\n- `message_start` 및 `message_end` 사용자, 보조자 및 도구 결과 메시지에 대해 실행됩니다.\n- `message_update` 어시스턴트 스트리밍 업데이트를 위해 실행됩니다.\n- `message_end` 핸들러는 `{ message }`를 반환하여 최종 메시지를 대체할 수 있습니다. 교체는 동일하게 유지되어야 합니다 `role`.\n\n```typescript\npi.on(\"message_start\", async (event, ctx) => {\n  // event.message\n});\n\npi.on(\"message_update\", async (event, ctx) => {\n  // event.message\n  // event.assistantMessageEvent (token-by-token stream event)\n});\n\npi.on(\"message_end\", async (event, ctx) => {\n  if (event.message.role !== \"assistant\") return;\n\n  return {\n    message: {\n      ...event.message,\n      usage: {\n        ...event.message.usage,\n        cost: {\n          ...event.message.usage.cost,\n          total: 0.123,\n        },\n      },\n    },\n  };\n});\n```\n\n#### tool_execution_start / tool_execution_update / tool_execution_end\n\n도구 실행 수명 주기 업데이트를 위해 시작됩니다.\n\n병렬 도구 모드에서:\n- `tool_execution_start`는 비행 전 단계에서 보조 소스 순서로 방출됩니다.\n- `tool_execution_update` 이벤트가 여러 도구에 걸쳐 인터리브될 수 있음\n- `tool_execution_end`는 각 도구가 완료된 후 도구 완료 순서대로 내보내집니다.\n- final `toolResult` 메시지 이벤트는 나중에 어시스턴트 소스 순서대로 방출됩니다.\n\n```typescript\npi.on(\"tool_execution_start\", async (event, ctx) => {\n  // event.toolCallId, event.toolName, event.args\n});\n\npi.on(\"tool_execution_update\", async (event, ctx) => {\n  // event.toolCallId, event.toolName, event.args, event.partialResult\n});\n\npi.on(\"tool_execution_end\", async (event, ctx) => {\n  // event.toolCallId, event.toolName, event.result, event.isError\n});\n```\n\n#### 문맥\n\n각 LLM 호출 전에 실행됩니다. 메시지를 비파괴적으로 수정합니다. 메시지 유형은 [Session Format](session-format.md)를 참조하세요.\n\n```typescript\npi.on(\"context\", async (event, ctx) => {\n  // event.messages - deep copy, safe to modify\n  const filtered = event.messages.filter(m => !shouldPrune(m));\n  return { messages: filtered };\n});\n```\n\n#### before_provider_headers\n\n나가는 HTTP 헤더가 조합된 후에 실행됩니다. 요청 헤더를 추가, 재정의 또는 제거하는 데 사용합니다.\n\n핸들러는 `event.headers`를 제자리에서 변경합니다. 추가하거나 재정의하려면 키를 문자열로 설정하고, 삭제하려면 `null`로 설정하세요.\n\n```typescript\npi.on(\"before_provider_headers\", (event, ctx) => {\n  // Add or override — e.g. a session id for gateway tracing/attribution\n  event.headers[\"x-session-id\"] = ctx.sessionManager.getSessionId();\n\n  // Drop a tracking header pi adds for this call\n  event.headers[\"X-OpenRouter-Title\"] = null;\n});\n```\n\n공급자 요청당 한 번 실행됩니다. 재시도는 후크를 다시 실행하는 대신 동일한 헤더를 재사용합니다.\n\n#### before_provider_request\n\n공급자별 페이로드가 빌드된 후 요청이 전송되기 직전에 실행됩니다. 처리기는 확장 로드 순서로 실행됩니다. `undefined`를 반환하면 페이로드가 변경되지 않은 상태로 유지됩니다. 다른 값을 반환하면 이후 처리기와 실제 요청에 대한 페이로드가 대체됩니다.\n\n이 후크는 공급자 수준 시스템 지침을 다시 작성하거나 완전히 제거할 수 있습니다. 이러한 페이로드 수준 변경 사항은 최종 직렬화된 공급자 페이로드가 아닌 Pi의 시스템 프롬프트 문자열을 보고하는 `ctx.getSystemPrompt()`에 반영되지 않습니다.\n\n```typescript\npi.on(\"before_provider_request\", (event, ctx) => {\n  console.log(JSON.stringify(event.payload, null, 2));\n\n  // Optional: replace payload\n  // return { ...event.payload, temperature: 0 };\n});\n```\n\n이는 주로 공급자 직렬화 및 캐시 동작을 디버깅하는 데 유용합니다.\n\n#### after_provider_response\n\nHTTP 응답이 수신된 후 스트림 본문이 사용되기 전에 실행됩니다. 처리기는 확장 로드 순서로 실행됩니다.\n\n```typescript\npi.on(\"after_provider_response\", (event, ctx) => {\n  // event.status - HTTP status code\n  // event.headers - normalized response headers\n  if (event.status === 429) {\n    console.log(\"rate limited\", event.headers[\"retry-after\"]);\n  }\n});\n```\n\n헤더 가용성은 공급자와 전송에 따라 다릅니다. Providers 추상 HTTP 응답은 헤더를 노출하지 않을 수 있습니다.\n\n### 모델 이벤트\n\n#### 모델_선택\n\n`/model` 명령, 모델 순환(`Ctrl+P`) 또는 세션 복원을 통해 모델이 변경되면 실행됩니다.\n\n```typescript\npi.on(\"model_select\", async (event, ctx) => {\n  // event.model - newly selected model\n  // event.previousModel - previous model (undefined if first selection)\n  // event.source - \"set\" | \"cycle\" | \"restore\"\n\n  const prev = event.previousModel\n    ? `${event.previousModel.provider}/${event.previousModel.id}`\n    : \"none\";\n  const next = `${event.model.provider}/${event.model.id}`;\n\n  ctx.ui.notify(`Model changed (${event.source}): ${prev} -> ${next}`, \"info\");\n});\n```\n\n이를 사용하여 UI 요소(상태 표시줄, 바닥글)를 업데이트하거나 활성 모델이 변경될 때 모델별 초기화를 수행합니다.\n\n#### Thinking_level_select\n\n사고 수준이 바뀔 때 실행됩니다. 이는 알림 전용입니다. 핸들러 반환 값은 무시됩니다.\n\n```typescript\npi.on(\"thinking_level_select\", async (event, ctx) => {\n  // event.level - newly selected thinking level\n  // event.previousLevel - previous thinking level\n\n  ctx.ui.setStatus(\"thinking\", `thinking: ${event.level}`);\n});\n```\n\n`pi.setThinkingLevel()`, 모델 변경 또는 내장된 사고 수준 컨트롤이 활성 사고 수준을 변경할 때 이를 사용하여 확장 UI를 업데이트합니다.\n\n### 도구 이벤트\n\n#### 도구 호출\n\n도구가 실행되기 전인 `tool_execution_start` 이후에 실행됩니다. **차단 가능.** 입력 범위를 좁히고 입력하려면 `isToolCallEventType`를 사용하세요.\n\n`tool_call`가 실행되기 전에 pi는 이전에 발생한 Agent 이벤트가 `AgentSession`을 통해 배수를 완료할 때까지 기다립니다. 이는 `ctx.sessionManager`가 현재 보조 도구 호출 메시지를 통해 최신 상태임을 의미합니다.\n\n기본 병렬 도구 실행 모드에서는 동일한 보조 메시지의 형제 도구 호출이 순차적으로 사전 실행된 다음 동시에 실행됩니다. `tool_call`는 `ctx.sessionManager`의 동일한 보조 메시지에서 형제 도구 결과를 볼 수 있다고 보장되지 않습니다.\n\n`event.input`는 변경 가능합니다. 실행 전에 도구 인수를 패치하려면 이를 변경하세요.\n\n동작 보장:\n- `event.input`에 대한 변형은 실제 도구 실행에 영향을 미칩니다.\n- 나중에 `tool_call` 핸들러는 이전 핸들러가 만든 변형을 봅니다.\n- 돌연변이 후에는 재검증이 수행되지 않습니다.\n- `{ block: true, reason?: string, terminate?: boolean }`을 통한 `tool_call` 제어 차단의 반환 값\n- `terminate` 차단된 통화에만 적용됩니다. 배치의 모든 최종 결과가 종료될 때만 에이전트가 일찍 중지됩니다.\n\n```typescript\nimport { isToolCallEventType } from \"@earendil-works/pi-coding-agent\";\n\npi.on(\"tool_call\", async (event, ctx) => {\n  // event.toolName - \"bash\", \"read\", \"write\", \"edit\", etc.\n  // event.toolCallId\n  // event.input - tool parameters (mutable)\n\n  // Built-in tools: no type params needed\n  if (isToolCallEventType(\"bash\", event)) {\n    // event.input is { command: string; timeout?: number }\n    event.input.command = `source ~/.profile\\n${event.input.command}`;\n\n    if (event.input.command.includes(\"rm -rf\")) {\n      return { block: true, reason: \"Dangerous command\", terminate: true };\n    }\n  }\n\n  if (isToolCallEventType(\"read\", event)) {\n    // event.input is { path: string; offset?: number; limit?: number }\n    console.log(`Reading: ${event.input.path}`);\n  }\n});\n```\n\n#### 사용자 정의 도구 입력 입력\n\n사용자 정의 도구는 입력 유형을 내보내야 합니다.\n\n```typescript\n// my-extension.ts\nexport type MyToolInput = Static<typeof myToolSchema>;\n```\n\n명시적인 유형 매개변수와 함께 `isToolCallEventType`를 사용하세요.\n\n```typescript\nimport { isToolCallEventType } from \"@earendil-works/pi-coding-agent\";\nimport type { MyToolInput } from \"my-extension\";\n\npi.on(\"tool_call\", (event) => {\n  if (isToolCallEventType<\"my_tool\", MyToolInput>(\"my_tool\", event)) {\n    event.input.action;  // typed\n  }\n});\n```\n\n#### 도구_결과\n\n도구 실행이 완료된 후 `tool_execution_end` 및 최종 도구 결과 메시지 이벤트가 발생하기 전에 실행됩니다. **결과를 수정할 수 있습니다.**\n\n병렬 도구 모드에서는 `tool_result` 및 `tool_execution_end`가 도구 완료 순서로 인터리브될 수 있지만 최종 `toolResult` 메시지 이벤트는 나중에 보조 소스 순서로 방출됩니다.\n\n`tool_result` 미들웨어와 같은 핸들러 체인:\n- 핸들러는 확장 로드 순서로 실행됩니다.\n- 각 핸들러는 이전 핸들러 변경 후 최신 결과를 확인합니다.\n- 핸들러는 부분 패치(`content`, `details`, `isError` 또는 `usage`)를 반환할 수 있습니다. 생략된 필드는 현재 값을 유지합니다.\n\n핸들러 내부의 중첩된 비동기 작업에는 `ctx.signal`를 사용하세요. 이를 통해 Esc는 모델 호출, `fetch()` 및 확장 프로그램에서 시작된 기타 중단 인식 작업을 취소할 수 있습니다.\n\n```typescript\nimport { isBashToolResult } from \"@earendil-works/pi-coding-agent\";\n\npi.on(\"tool_result\", async (event, ctx) => {\n  // event.toolName, event.toolCallId, event.input\n  // event.content, event.details, event.isError, event.usage\n\n  if (isBashToolResult(event)) {\n    // event.details is typed as BashToolDetails\n  }\n\n  const response = await fetch(\"https://example.com/summarize\", {\n    method: \"POST\",\n    body: JSON.stringify({ content: event.content }),\n    signal: ctx.signal,\n  });\n\n  // Modify result:\n  return { content: [...], details: {...}, isError: false, usage: nestedModelUsage };\n});\n```\n\n### 사용자 배쉬 이벤트\n\n#### user_bash\n\n사용자가 `!` 또는 `!!` 명령을 실행하면 시작됩니다. **요격할 수 있습니다.**\n\n```typescript\nimport { createLocalBashOperations } from \"@earendil-works/pi-coding-agent\";\n\npi.on(\"user_bash\", (event, ctx) => {\n  // event.command - the bash command\n  // event.excludeFromContext - true if !! prefix\n  // event.cwd - working directory\n\n  // Option 1: Provide custom operations (e.g., SSH)\n  return { operations: remoteBashOps };\n\n  // Option 2: Wrap pi's built-in local bash backend\n  const local = createLocalBashOperations();\n  return {\n    operations: {\n      exec(command, cwd, options) {\n        return local.exec(`source ~/.profile\\n${command}`, cwd, options);\n      }\n    }\n  };\n\n  // Option 3: Full replacement - return result directly\n  return { result: { output: \"...\", exitCode: 0, cancelled: false, truncated: false } };\n});\n```\n\n### 입력 이벤트\n\n#### 입력\n\n확장 명령을 확인한 후 스킬 및 템플릿 확장 전에 사용자 입력이 수신되면 실행됩니다. 이벤트는 원시 입력 텍스트를 확인하므로 `/skill:foo` 및 `/template`는 아직 확장되지 않습니다.\n\n**처리 순서:**\n1. 확장 명령(`/cmd`)이 먼저 확인됩니다. 발견되면 핸들러가 실행되고 입력 이벤트를 건너뜁니다.\n2. `input` 이벤트 발생 - 가로채기, 변형 또는 처리 가능\n3. 처리하지 않을 경우: 스킬 명령어(`/skill:name`)를 스킬 내용으로 확장\n4. 처리되지 않은 경우: prompt templates(`/template`) 템플릿 콘텐츠로 확장\n5. 에이전트 처리 시작(`before_agent_start` 등)\n\n```typescript\npi.on(\"input\", async (event, ctx) => {\n  // event.text - raw input (before skill/template expansion)\n  // event.images - attached images, if any\n  // event.source - \"interactive\" (typed), \"rpc\" (API), or \"extension\" (via sendUserMessage)\n  // event.streamingBehavior - \"steer\" | \"followUp\" | undefined\n  //   undefined when idle, \"steer\" for mid-stream interrupts,\n  //   \"followUp\" for messages queued until the agent finishes\n\n  // Transform: rewrite input before expansion\n  if (event.text.startsWith(\"?quick \"))\n    return { action: \"transform\", text: `Respond briefly: ${event.text.slice(7)}` };\n\n  // Handle: respond without LLM (extension shows its own feedback)\n  if (event.text === \"ping\") {\n    ctx.ui.notify(\"pong\", \"info\");\n    return { action: \"handled\" };\n  }\n\n  // Route by source: skip processing for extension-injected messages\n  if (event.source === \"extension\") return { action: \"continue\" };\n\n  // Intercept skill commands before expansion\n  if (event.text.startsWith(\"/skill:\")) {\n    // Could transform, block, or let pass through\n  }\n\n  return { action: \"continue\" };  // Default: pass through to expansion\n});\n```\n\n**결과:**\n- `continue` - 변경되지 않은 상태로 통과(핸들러가 아무것도 반환하지 않는 경우 기본값)\n- `transform` - 텍스트/이미지를 수정한 후 계속 확장합니다.\n- `handled` - 에이전트를 완전히 건너뜁니다(이를 반환하는 첫 번째 핸들러가 승리합니다).\n\n핸들러 전체에서 체인을 변환합니다. `streamingBehavior` 인식 라우팅은 [input-transform.ts](../examples/extensions/input-transform.ts) 및 [input-transform-streaming.ts](../examples/extensions/input-transform-streaming.ts)를 참조하세요.\n\n## 확장 컨텍스트\n\n모든 핸들러는 `ctx: ExtensionContext`를 받습니다.\n\n### ctx.ui\n\n사용자 상호작용을 위한 UI 메소드. 자세한 내용은 [Custom UI](#custom-ui)를 참조하세요.\n\n### ctx.모드\n\n현재 실행 모드: `\"tui\"`, `\"rpc\"`, `\"json\"` 또는 `\"print\"`. `ctx.mode === \"tui\"`를 사용하면 `custom()`, 구성 요소 팩토리, 터미널 입력 및 직접 TUI 렌더링과 같은 터미널 전용 기능을 보호할 수 있습니다.\n\n### ctx.hasUI\n\n`true` TUI 및 RPC 모드. `false` 인쇄 모드(`-p`) 및 JSON 모드. TUI 및 TUI 모두에서 작동하는 대화 방법(`select`, `confirm`, `input`, `editor`)과 실행 후 잊어버리는 방법(`notify`, `setStatus`, `setWidget`, `setTitle`, `setEditorText`)을 보호하려면 이 기능을 사용하세요. RPC 모드. RPC 모드에서 일부 TUI 관련 메서드는 작동하지 않거나 기본값을 반환합니다([rpc.md](rpc.md#extension-ui-protocol) 참조).\n\n### ctx.cwd\n\n현재 작업 디렉토리.\n\n프로젝트-로컬 구성 경로를 구성할 때 하드코딩 `.pi` 대신 `CONFIG_DIR_NAME`를 사용하세요. 브랜드가 변경된 배포판은 다른 구성 디렉터리 이름을 사용할 수 있습니다.\n\n```typescript\nimport { CONFIG_DIR_NAME, type ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { join } from \"node:path\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.on(\"session_start\", (_event, ctx) => {\n    const projectConfigPath = join(ctx.cwd, CONFIG_DIR_NAME, \"my-extension.json\");\n    // ...\n  });\n}\n```\n\n### ctx.isProjectTrusted()\n\n현재 세션 컨텍스트에 대해 프로젝트-로컬 신뢰가 활성화되어 있는지 여부를 반환합니다. 여기에는 글로벌 신뢰 저장소에 저장된 결정뿐만 아니라 임시 신뢰 결정 및 CLI 신뢰 재정의도 포함됩니다.\n\n신뢰할 수 있는 프로젝트에 대해서만 적용되어야 하는 프로젝트-로컬 확장 구성을 읽기 전에 이를 사용하세요.\n\n### ctx.sessionManager\n\n세션 상태에 대한 읽기 전용 액세스입니다. 전체 SessionManager API 및 항목 유형은 [Session Format](session-format.md)를 참조하세요.\n\n`tool_call`의 경우 이 상태는 핸들러가 실행되기 전에 현재 보조 메시지를 통해 동기화됩니다. 병렬 도구 실행 모드에서는 여전히 동일한 보조 메시지의 형제 도구 결과가 포함된다는 보장이 없습니다.\n\n```typescript\nctx.sessionManager.getEntries()             // All entries\nctx.sessionManager.getBranch()              // Current branch\nctx.sessionManager.buildContextEntries()    // Active branch entries with compaction applied\nctx.sessionManager.getLeafId()              // Current leaf entry ID\n```\n\n### ctx.modelRegistry / ctx.model / ctx.thinkingLevel / ctx.scopedModels\n\n모델, 공급자 및 해결된 인증에 대한 액세스입니다. `ctx.modelRegistry.getProvider(id)`는 효과적인 pi-ai 공급자를 반환하는 반면, `getProviderAuth(id)`는 로드된 모델을 요구하지 않고 현재 API key, 헤더, 기본 URL 및 공급자 범위 환경을 해결합니다. `ctx.model`는 능동적 모델이고, `ctx.thinkingLevel`는 현재 효과적인 사고 수준입니다.\n\n`ctx.scopedModels`는 현재 세션으로 범위가 지정된 모델의 읽기 전용 목록입니다. `/scoped-models` 명령이 표시하는 것과 동일한 세트입니다. 세션 시작 시 `--models` CLI 플래그 및 `enabledModels` 설정(`provider/modelId` 또는 베어 `modelId`의 미니매치가 있는 사용 가능한 카탈로그와 일치)에서 해결됩니다. 범위 지정이 구성되지 않은 경우 비어 있습니다. 즉, 사용 가능한 모든 모델을 사용할 수 있음을 의미합니다. 각 항목은 `{ model, thinkingLevel? }`이며, 여기서 `thinkingLevel`는 패턴이 고정된 경우에만 설정됩니다(예: `anthropic/*:high`). `ctx.modelRegistry.getAvailable()`를 통해 전체 카탈로그를 열거하는 대신 내장 모델을 미러링하는 모델 선택기를 채우는 데 사용하세요.\n\n### ctx.신호\n\n현재 에이전트 중단 신호 또는 에이전트 차례가 활성화되지 않은 경우 `undefined`입니다.\n\n확장 핸들러에 의해 시작된 중단 인식 중첩 작업에 이를 사용하십시오. 예를 들면 다음과 같습니다.\n- `fetch(..., { signal: ctx.signal })`\n- `signal`을 허용하는 모델 호출\n- `AbortSignal`을 허용하는 파일 또는 프로세스 도우미\n\n`ctx.signal`는 일반적으로 `tool_call`, `tool_result`, `message_update` 및 `turn_end`와 같은 활성 턴 이벤트 중에 정의됩니다.\n세션 이벤트, 확장 명령, pi가 유휴 상태인 동안 실행되는 바로 가기와 같은 유휴 또는 비순차 컨텍스트에서는 일반적으로 `undefined`입니다.\n\n```typescript\npi.on(\"tool_result\", async (event, ctx) => {\n  const response = await fetch(\"https://example.com/api\", {\n    method: \"POST\",\n    body: JSON.stringify(event),\n    signal: ctx.signal,\n  });\n\n  const data = await response.json();\n  return { details: data };\n});\n```\n\n### ctx.isIdle() / ctx.abort() / ctx.hasPendingMessages()\n\n제어 흐름 도우미. `ctx.isIdle()`는 Pi이 에이전트 실행, 자동 재시도, 자동 압축 재시도 또는 대기 중인 연속을 처리하는 동안 false입니다.\n\n### ctx.shutdown()\n\npi의 정상적인 종료를 요청합니다.\n\n- **대화형 모드:** 에이전트가 유휴 상태가 될 때까지 연기됩니다(대기 중인 조정 및 후속 메시지를 모두 처리한 후).\n- **RPC 모드:** 다음 유휴 상태까지 연기됩니다(현재 명령 응답 완료 후, 다음 명령을 기다리는 경우).\n- **인쇄 모드:** 작동하지 않습니다. 모든 프롬프트가 처리되면 프로세스가 자동으로 종료됩니다.\n\n종료하기 전에 모든 확장 프로그램에 `session_shutdown` 이벤트를 발생시킵니다. 모든 컨텍스트(이벤트 처리기, 도구, 명령, 바로 가기)에서 사용할 수 있습니다.\n\n```typescript\npi.on(\"tool_call\", (event, ctx) => {\n  if (isFatal(event.input)) {\n    ctx.shutdown();\n  }\n});\n```\n\n### ctx.getContextUsage()\n\n활성 모델의 현재 컨텍스트 사용량을 반환합니다. 가능한 경우 마지막 어시스턴트 사용량을 사용한 다음 후행 메시지에 대한 토큰을 추정합니다.\n\n```typescript\nconst usage = ctx.getContextUsage();\nif (usage && usage.tokens > 100_000) {\n  // ...\n}\n```\n\n### ctx.compact()\n\n완료를 기다리지 않고 압축을 트리거합니다. 후속 작업에는 `onComplete` 및 `onError`를 사용하세요.\n\n```typescript\nctx.compact({\n  customInstructions: \"Focus on recent changes\",\n  onComplete: (result) => {\n    ctx.ui.notify(\"Compaction completed\", \"info\");\n  },\n  onError: (error) => {\n    ctx.ui.notify(`Compaction failed: ${error.message}`, \"error\");\n  },\n});\n```\n\n### ctx.getSystemPrompt()\n\nPi의 현재 시스템 프롬프트 문자열을 반환합니다.\n\n- `before_agent_start` 동안 이는 현재 턴에 대해 지금까지 적용된 체인 시스템 프롬프트 변경 사항을 반영합니다.\n- 이후 `context` 메시지 변형은 포함되지 않습니다.\n- `before_provider_request` 페이로드 재작성은 포함되지 않습니다.\n- 나중에 로드된 확장 프로그램이 사용자 확장 프로그램 이후에 실행되는 경우 최종적으로 전송되는 내용이 여전히 변경될 수 있습니다.\n\n```typescript\npi.on(\"before_agent_start\", (event, ctx) => {\n  const prompt = ctx.getSystemPrompt();\n  console.log(`System prompt length: ${prompt.length}`);\n});\n```\n\n## ExtensionCommandContext\n\n명령 핸들러는 세션 제어 방법으로 `ExtensionContext`를 확장하는 `ExtensionCommandContext`를 수신합니다. 이벤트 핸들러에서 호출하면 교착 상태가 발생할 수 있으므로 명령에서만 사용할 수 있습니다.\n\n### ctx.getSystemPromptOptions()\n\nPi가 현재 시스템 프롬프트를 구축하는 데 사용하는 기본 입력을 반환합니다.\n\n```typescript\nconst options = ctx.getSystemPromptOptions();\nconst contextPaths = options.contextFiles?.map((file) => file.path) ?? [];\n```\n\n이는 `before_agent_start` `event.systemPromptOptions`와 모양 및 변경 가능성이 동일합니다: 사용자 정의 프롬프트, 활성 도구, 도구 조각, 프롬프트 지침, 추가된 시스템 프롬프트 텍스트, cwd, 로드된 context files 및 로드된 기술. 전체 컨텍스트 파일 콘텐츠가 포함될 수 있으므로 민감한 확장 로컬 데이터로 취급하고 명령 목록, 로그 또는 자동 완성 메타데이터를 통해 노출되지 않도록 하세요.\n\n이는 현재 기본 프롬프트 입력을 보고합니다. 턴별 `before_agent_start` 연결된 시스템 프롬프트 변경, 이후 `context` 이벤트 메시지 변형 또는 `before_provider_request` 페이로드 재작성은 포함되지 않습니다.\n\n### ctx.waitForIdle()\n\n자동 재시도, 자동 압축 재시도 및 대기 중인 연속 작업을 포함하여 에이전트가 완전히 해결될 때까지 기다립니다.\n\n```typescript\npi.registerCommand(\"my-cmd\", {\n  handler: async (args, ctx) => {\n    await ctx.waitForIdle();\n    // Agent is now idle, safe to modify session\n  },\n});\n```\n\n### ctx.newSession(옵션?)\n\n새 세션을 만듭니다.\n\n```typescript\nconst parentSession = ctx.sessionManager.getSessionFile();\nconst kickoff = \"Continue in the replacement session\";\n\nconst result = await ctx.newSession({\n  parentSession,\n  setup: async (sm) => {\n    sm.appendMessage({\n      role: \"user\",\n      content: [{ type: \"text\", text: \"Context from previous session...\" }],\n      timestamp: Date.now(),\n    });\n  },\n  withSession: async (ctx) => {\n    // Use only the replacement-session ctx here.\n    await ctx.sendUserMessage(kickoff);\n  },\n});\n\nif (result.cancelled) {\n  // An extension cancelled the new session\n}\n```\n\n옵션:\n- `parentSession`: 새 세션 헤더에 기록할 상위 세션 파일\n- `setup`: `withSession`가 실행되기 전에 새 세션의 `SessionManager`를 변경합니다.\n- `withSession`: 새로운 교체 세션 컨텍스트에 대해 전환 후 작업을 실행합니다. 캡처된 이전 `pi` / 명령 `ctx`을 사용하지 마세요. [Session replacement lifecycle and footguns](#session-replacement-lifecycle-and-footguns)를 참조하세요.\n\n### ctx.fork(entryId, 옵션?)\n\n특정 항목에서 분기하여 새 세션 파일을 만듭니다.\n\n```typescript\nconst result = await ctx.fork(\"entry-id-123\", {\n  withSession: async (ctx) => {\n    // Use only the replacement-session ctx here.\n    ctx.ui.notify(\"Now in the forked session\", \"info\");\n  },\n});\nif (result.cancelled) {\n  // An extension cancelled the fork\n}\n\nconst cloneResult = await ctx.fork(\"entry-id-456\", { position: \"at\" });\nif (cloneResult.cancelled) {\n  // An extension cancelled the clone\n}\n```\n\n옵션:\n- `position`: `\"before\"`(기본값) 선택한 사용자 메시지 앞에서 포크하여 해당 메시지를 편집기에 복원합니다.\n- `position`: `\"at\"` 편집기 텍스트를 복원하지 않고 선택한 항목을 통해 활성 경로를 복제합니다.\n- `withSession`: 새로운 교체 세션 컨텍스트에 대해 전환 후 작업을 실행합니다. 캡처된 이전 `pi` / 명령 `ctx`을 사용하지 마세요. [Session replacement lifecycle and footguns](#session-replacement-lifecycle-and-footguns)를 참조하세요.\n\n### ctx.navigateTree(targetId, 옵션?)\n\nsession tree에서 다른 지점으로 이동합니다.\n\n```typescript\nconst result = await ctx.navigateTree(\"entry-id-456\", {\n  summarize: true,\n  customInstructions: \"Focus on error handling changes\",\n  replaceInstructions: false, // true = replace default prompt entirely\n  label: \"review-checkpoint\",\n});\n```\n\n옵션:\n- `summarize`: 버려진 브랜치의 요약을 생성할지 여부\n- `customInstructions`: 요약기에 대한 사용자 정의 지침\n- `replaceInstructions`: true인 경우 `customInstructions`는 추가되는 대신 기본 프롬프트를 대체합니다.\n- `label`: 분기 요약 항목(또는 요약하지 않는 경우 대상 항목)에 첨부할 라벨\n\n### ctx.switchSession(세션 경로, 옵션?)\n\n다른 세션 파일로 전환합니다.\n\n```typescript\nconst result = await ctx.switchSession(\"/path/to/session.jsonl\", {\n  withSession: async (ctx) => {\n    await ctx.sendUserMessage(\"Resume work in the replacement session\");\n  },\n});\nif (result.cancelled) {\n  // An extension cancelled the switch via session_before_switch\n}\n```\n\n옵션:\n- `withSession`: 새로운 교체 세션 컨텍스트에 대해 전환 후 작업을 실행합니다. 캡처된 이전 `pi` / 명령 `ctx`을 사용하지 마세요. [Session replacement lifecycle and footguns](#session-replacement-lifecycle-and-footguns)를 참조하세요.\n\n사용 가능한 세션을 검색하려면 정적 `SessionManager.list()` 또는 `SessionManager.listAll()` 메소드를 사용하세요.\n\n```typescript\nimport { SessionManager } from \"@earendil-works/pi-coding-agent\";\n\npi.registerCommand(\"switch\", {\n  description: \"Switch to another session\",\n  handler: async (args, ctx) => {\n    const sessions = await SessionManager.list(ctx.cwd);\n    if (sessions.length === 0) return;\n    const choice = await ctx.ui.select(\n      \"Pick session:\",\n      sessions.map(s => s.file),\n    );\n    if (choice) {\n      await ctx.switchSession(choice, {\n        withSession: async (ctx) => {\n          ctx.ui.notify(\"Switched session\", \"info\");\n        },\n      });\n    }\n  },\n});\n```\n\n### 세션 교체 수명주기 및 풋건\n\n`withSession`는 대체 세션에 바인딩된 비동기 `sendMessage()` 및 `sendUserMessage()` 도우미로 `ExtensionCommandContext`를 확장하는 새로운 `ReplacedSessionContext`를 받습니다.\n\n라이프사이클 및 풋건:\n- `withSession`는 이전 세션이 실행되고(`session_shutdown`), 이전 런타임이 해제되고, 대체 세션이 리바운드되고, 새 확장 인스턴스가 이미 `session_start`를 수신한 후에만 실행됩니다.\n- 콜백은 새 확장 인스턴스 내부가 아닌 원래 클로저에서 계속 실행됩니다. 이는 `withSession`가 시작되기 전에 이전 확장 프로그램 인스턴스가 이미 종료 정리를 실행했을 수 있음을 의미합니다.\n- 캡처된 이전 `pi` / 이전 명령 `ctx` 세션 바인딩 개체는 교체 후 오래되었으며 사용하면 오류가 발생합니다. 세션 바인딩 작업의 경우 `withSession`에 전달된 `ctx`만 사용하세요.\n- 이전에 추출한 원시 개체는 여전히 귀하의 책임입니다. 예를 들어 교체하기 전에 `const sm = ctx.sessionManager`를 캡처하면 `sm`는 여전히 이전 `SessionManager` 개체입니다. 교체 후 재사용하지 마십시오.\n- `withSession`의 코드는 `session_shutdown` 핸들러에 의해 무효화된 상태가 이미 사라진 것으로 가정해야 합니다. 문자열, ID, 직렬화된 구성 등 종료 후에도 깨끗하게 유지되는 일반 데이터만 캡처합니다.\n\n안전한 패턴:\n\n```typescript\npi.registerCommand(\"handoff\", {\n  handler: async (_args, ctx) => {\n    const kickoff = \"Continue from the replacement session\";\n    await ctx.newSession({\n      withSession: async (ctx) => {\n        await ctx.sendUserMessage(kickoff);\n      },\n    });\n  },\n});\n```\n\n안전하지 않은 패턴:\n\n```typescript\npi.registerCommand(\"handoff\", {\n  handler: async (_args, ctx) => {\n    const oldSessionManager = ctx.sessionManager;\n    await ctx.newSession({\n      withSession: async (_ctx) => {\n        // stale old objects: do not do this\n        oldSessionManager.getSessionFile();\n        pi.sendUserMessage(\"wrong\");\n      },\n    });\n  },\n});\n```\n\n### ctx.reload()\n\n`/reload`와 동일한 다시 로드 흐름을 실행합니다.\n\n```typescript\npi.registerCommand(\"reload-runtime\", {\n  description: \"Reload extensions, skills, prompts, themes, and context files\",\n  handler: async (_args, ctx) => {\n    await ctx.reload();\n    return;\n  },\n});\n```\n\n중요한 동작:\n- `await ctx.reload()`는 현재 확장 런타임에 대해 `session_shutdown`을 내보냅니다.\n- 그런 다음 리소스를 다시 로드하고 `reason: \"reload\"`로 `session_start`을 내보내고 이유 `\"reload\"`로 `resources_discover`를 내보냅니다.\n- 현재 실행 중인 명령 처리기는 여전히 이전 호출 프레임에서 계속됩니다.\n- `await ctx.reload()` 이후의 코드는 다시 로드 전 버전에서 계속 실행됩니다.\n- `await ctx.reload()` 이후의 코드는 이전 메모리 내 확장 상태가 여전히 유효하다고 가정하면 안 됩니다.\n- 핸들러가 반환된 후 향후 명령/이벤트/도구 호출에서는 새 확장 버전을 사용합니다.\n\n예측 가능한 동작을 위해 다시 로드를 해당 핸들러의 터미널로 처리하십시오(`await ctx.reload(); return;`).\n\n도구는 `ExtensionContext`로 실행되므로 `ctx.reload()`를 직접 호출할 수 없습니다. 명령을 다시 로드 진입점으로 사용한 다음 해당 명령을 후속 사용자 메시지로 대기열에 추가하는 도구를 노출합니다.\n\nLLM이 다시 로드를 트리거하기 위해 호출할 수 있는 예제 도구:\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { Type } from \"typebox\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerCommand(\"reload-runtime\", {\n    description: \"Reload extensions, skills, prompts, themes, and context files\",\n    handler: async (_args, ctx) => {\n      await ctx.reload();\n      return;\n    },\n  });\n\n  pi.registerTool({\n    name: \"reload_runtime\",\n    label: \"Reload Runtime\",\n    description: \"Reload extensions, skills, prompts, themes, and context files\",\n    parameters: Type.Object({}),\n    async execute() {\n      pi.sendUserMessage(\"/reload-runtime\", { deliverAs: \"followUp\" });\n      return {\n        content: [{ type: \"text\", text: \"Queued /reload-runtime as a follow-up command.\" }],\n      };\n    },\n  });\n}\n```\n\n## 확장API 방법\n\n### pi.on(이벤트, 핸들러)\n\n이벤트를 구독하세요. 이벤트 유형 및 반환 값은 [Events](#events)를 참조하세요.\n\n### pi.registerTool(정의)\n\nLLM에서 호출할 수 있는 사용자 정의 도구를 등록합니다. 자세한 내용은 [Custom Tools](#custom-tools)를 참조하세요.\n\n`pi.registerTool()` 확장 로드 중과 시작 후에 모두 작동합니다. `session_start`, 명령 핸들러 또는 기타 이벤트 핸들러 내에서 호출할 수 있습니다. 새로운 도구는 동일한 세션에서 즉시 새로 고쳐지므로 `pi.getAllTools()`에 표시되고 `/reload` 없이 LLM에서 호출할 수 있습니다.\n\n런타임에 도구(동적으로 추가된 도구 포함)를 활성화하거나 비활성화하려면 `pi.setActiveTools()`를 사용하세요.\n\n`promptSnippet`를 사용하여 `Available tools`의 한 줄 항목에 사용자 정의 도구를 선택하고, `promptGuidelines`를 사용하여 도구가 활성화되었을 때 기본 `Guidelines` 섹션에 도구별 글머리 기호를 추가합니다.\n\n**중요:** `promptGuidelines` 글머리 기호는 도구 이름 접두사 없이 `Guidelines` 섹션에 추가됩니다. 각 지침은 참조하는 도구의 이름을 지정해야 합니다. LLM은 \"이\"가 어떤 도구를 의미하는지 알 수 없기 때문에 \"...일 때 이 도구를 사용하십시오.\"를 피하십시오. 대신 \"...일 때 my_tool 사용\"이라고 작성하세요.\n\n전체 예를 보려면 [dynamic-tools.ts](../examples/extensions/dynamic-tools.ts)를 참조하세요.\n\n```typescript\nimport { Type } from \"typebox\";\nimport { StringEnum } from \"@earendil-works/pi-ai\";\n\npi.registerTool({\n  name: \"my_tool\",\n  label: \"My Tool\",\n  description: \"What this tool does\",\n  promptSnippet: \"Summarize or transform text according to action\",\n  promptGuidelines: [\"Use my_tool when the user asks to summarize previously generated text.\"],\n  parameters: Type.Object({\n    action: StringEnum([\"list\", \"add\"] as const),\n    text: Type.Optional(Type.String()),\n  }),\n  prepareArguments(args) {\n    // Optional compatibility shim. Runs before schema validation.\n    // Return the current schema shape, for example to fold legacy fields\n    // into the modern parameter object.\n    return args;\n  },\n\n  async execute(toolCallId, params, signal, onUpdate, ctx) {\n    // Stream progress\n    onUpdate?.({ content: [{ type: \"text\", text: \"Working...\" }] });\n\n    return {\n      content: [{ type: \"text\", text: \"Done\" }],\n      details: { result: \"...\" },\n    };\n  },\n\n  // Optional: Custom rendering\n  renderCall(args, theme, context) { ... },\n  renderResult(result, options, theme, context) { ... },\n});\n```\n\n### pi.sendMessage(메시지, 옵션?)\n\n세션에 사용자 정의 메시지를 삽입합니다. 사용자 정의 메시지는 LLM 컨텍스트에 참여합니다. LLM으로 보내면 안 되는 내구성 있는 TUI 전용 콘텐츠의 경우 [`pi.appendEntry()`](#piappendentrycustomtype-data)를 [`pi.registerEntryRenderer()`](#piregisterentryrenderercustomtype-renderer)와 함께 사용하세요.\n\n```typescript\npi.sendMessage({\n  customType: \"my-extension\",\n  content: \"Message text\",\n  display: true,\n  details: { ... },\n}, {\n  triggerTurn: true,\n  deliverAs: \"steer\",\n});\n```\n\n**옵션:**\n- `deliverAs` - 전달 모드:\n  - `\"steer\"`(기본값) - 스트리밍하는 동안 메시지를 대기열에 넣습니다. 현재 보조 차례가 도구 호출 실행을 마친 후 다음 LLM 호출 전에 전달됩니다.\n  - `\"followUp\"` - 에이전트가 완료될 때까지 기다립니다. 상담원이 더 이상 도구 호출을 하지 않는 경우에만 전달됩니다.\n  - `\"nextTurn\"` - 다음 사용자 프롬프트를 위해 대기 중입니다. 아무것도 방해하거나 트리거하지 않습니다.\n- `triggerTurn: true` - 에이전트가 유휴 상태인 경우 즉시 LLM 응답을 트리거합니다. `\"steer\"` 및 `\"followUp\"` 모드에만 적용됩니다(`\"nextTurn\"`의 경우 무시됨).\n\n### pi.sendUserMessage(콘텐츠, 옵션?)\n\n사용자 메시지를 에이전트에 보냅니다. 맞춤 메시지를 보내는 `sendMessage()`와 달리, 사용자가 입력한 것처럼 실제 사용자에게 메시지를 보냅니다. 항상 회전을 유발합니다.\n\n```typescript\n// Simple text message\npi.sendUserMessage(\"What is 2+2?\");\n\n// With content array (text + images)\npi.sendUserMessage([\n  { type: \"text\", text: \"Describe this image:\" },\n  { type: \"image\", source: { type: \"base64\", mediaType: \"image/png\", data: \"...\" } },\n]);\n\n// During streaming - must specify delivery mode\npi.sendUserMessage(\"Focus on error handling\", { deliverAs: \"steer\" });\npi.sendUserMessage(\"And then summarize\", { deliverAs: \"followUp\" });\n```\n\n**옵션:**\n- `deliverAs` - 에이전트가 스트리밍 중일 때 필수:\n  - `\"steer\"` - 현재 보조 턴이 도구 호출 실행을 마친 후 전달을 위해 메시지를 대기열에 넣습니다.\n  - `\"followUp\"` - 상담원이 모든 도구를 완료할 때까지 기다립니다.\n\n스트리밍하지 않을 때는 메시지가 즉시 전송되고 새로운 차례가 시작됩니다. `deliverAs` 없이 스트리밍하면 오류가 발생합니다.\n\n전체 예를 보려면 [send-user-message.ts](../examples/extensions/send-user-message.ts)를 참조하세요.\n\n### pi.appendEntry(customType, 데이터?)\n\n확장 데이터를 유지합니다. 사용자 정의 항목은 LLM 컨텍스트에 참여하지 않습니다. 대화형 모드에서는 `pi.registerEntryRenderer()`와 함께 사용하면 채팅 내용 내에서 렌더링할 수도 있습니다.\n\n```typescript\npi.appendEntry(\"my-state\", { count: 42 });\npi.appendEntry(\"status-card\", { title: \"Indexed files\", count: 17 });\n\n// Restore on reload\npi.on(\"session_start\", async (_event, ctx) => {\n  for (const entry of ctx.sessionManager.getEntries()) {\n    if (entry.type === \"custom\" && entry.customType === \"my-state\") {\n      // Reconstruct from entry.data\n    }\n  }\n});\n```\n\n### pi.setSessionName(이름)\n\n세션 표시 이름을 설정합니다(첫 번째 메시지 대신 세션 선택기에 표시됨).\n\n```typescript\npi.setSessionName(\"Refactor auth module\");\n```\n\n### pi.getSessionName()\n\n설정된 경우 현재 세션 이름을 가져옵니다.\n\n```typescript\nconst name = pi.getSessionName();\nif (name) {\n  console.log(`Session: ${name}`);\n}\n```\n\n### pi.setLabel(entryId, 라벨)\n\n항목의 레이블을 설정하거나 지웁니다. 라벨은 북마크 및 탐색을 위한 사용자 정의 마커입니다(`/tree` 선택기에 표시됨).\n\n```typescript\n// Set a label\npi.setLabel(entryId, \"checkpoint-before-refactor\");\n\n// Clear a label\npi.setLabel(entryId, undefined);\n\n// Read labels via sessionManager\nconst label = ctx.sessionManager.getLabel(entryId);\n```\n\n레이블은 세션에서 유지되며 다시 시작해도 유지됩니다. 대화 트리에서 중요한 지점(전환, 체크포인트)을 표시하는 데 사용합니다.\n\n### pi.registerCommand(이름, 옵션)\n\n명령어를 등록합니다.\n\n여러 확장이 동일한 명령 이름을 등록하는 경우 pi는 이를 모두 유지하고 로드 순서에 따라 숫자 호출 접미사를 할당합니다(예: `/review:1` 및 `/review:2`).\n\n```typescript\npi.registerCommand(\"stats\", {\n  description: \"Show session statistics\",\n  handler: async (args, ctx) => {\n    const count = ctx.sessionManager.getEntries().length;\n    ctx.ui.notify(`${count} entries`, \"info\");\n  }\n});\n```\n\n선택 사항: `/command...`에 대한 인수 자동 완성을 추가합니다.\n\n```typescript\nimport type { AutocompleteItem } from \"@earendil-works/pi-tui\";\n\npi.registerCommand(\"deploy\", {\n  description: \"Deploy to an environment\",\n  getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {\n    const envs = [\"dev\", \"staging\", \"prod\"];\n    const items = envs.map((e) => ({ value: e, label: e }));\n    const filtered = items.filter((i) => i.value.startsWith(prefix));\n    return filtered.length > 0 ? filtered : null;\n  },\n  handler: async (args, ctx) => {\n    ctx.ui.notify(`Deploying: ${args}`, \"info\");\n  },\n});\n```\n\n### pi.getCommands()\n\n현재 세션에서 `prompt`를 통해 호출 가능한 slash commands을 가져옵니다. 확장 명령어, prompt templates, 스킬 명령어가 포함됩니다.\n목록은 RPC `get_commands` 순서와 일치합니다. 확장 기능이 먼저, 템플릿이 먼저, 기술이 그 다음입니다.\n\n```typescript\nconst commands = pi.getCommands();\nconst bySource = commands.filter((command) => command.source === \"extension\");\nconst userScoped = commands.filter((command) => command.sourceInfo.scope === \"user\");\n```\n\n각 항목의 모양은 다음과 같습니다.\n\n```typescript\n{\n  name: string; // Invokable command name without the leading slash. May be suffixed like \"review:1\"\n  description?: string;\n  source: \"extension\" | \"prompt\" | \"skill\";\n  sourceInfo: {\n    path: string;\n    source: string;\n    scope: \"user\" | \"project\" | \"temporary\";\n    origin: \"package\" | \"top-level\";\n    baseDir?: string;\n  };\n}\n```\n\n`sourceInfo`를 정식 출처 필드로 사용하세요. 명령 이름이나 임시 경로 구문 분석에서 소유권을 추론하지 마십시오.\n\n내장된 대화형 명령(예: `/model` 및 `/settings`)은 여기에 포함되지 않습니다. 대화형으로만 처리됩니다.\n모드이며 `prompt`를 통해 전송되면 실행되지 않습니다.\n\n### pi.registerMessageRenderer(customType, 렌더러)\n\n`customType`에 사용자 정의 메시지를 위한 사용자 정의 TUI 렌더러를 등록하세요. 맞춤 메시지는 `pi.sendMessage()`를 사용하여 생성되고 LLM 컨텍스트에 참여합니다. [Custom UI](#custom-ui)를 참조하세요.\n\n### pi.registerMarkdownTransformer(변압기)\n\n일반 사용자 텍스트, 보조 텍스트 및 사고 블록에 Markdown에 대한 변환기를 등록합니다. Transformer는 확장 로드 순서로 실행되며 각 Transformer는 이전 Transformer에서 반환된 Markdown를 받습니다. 체인이 완료된 후 Pi는 내장 렌더러를 사용하여 변환된 콘텐츠를 렌더링합니다.\n\n변환기는 Markdown 문자열과 다음과 같은 컨텍스트를 수신합니다.\n\n- `messageType` — `\"user\"`, `\"assistant\"` 또는 `\"assistant-thinking\"`\n- `isStreaming` — `true` 부분 어시스턴트 업데이트의 경우; `false` 사용자, 최종 어시스턴트, 복원된 메시지용\n- `availableWidth` — 변환된 Markdown 콘텐츠에 사용할 수 있는 정확한 터미널 열\n\n변환된 Markdown를 반환합니다.\n\n```typescript\npi.registerMarkdownTransformer((markdown, { messageType, isStreaming }) => {\n  if (isStreaming || messageType === \"assistant-thinking\") return markdown;\n  return markdown.replaceAll(\"-->\", \"→\");\n});\n```\n\n변환기가 던지면 Pi는 지금까지 생성된 Markdown을 유지하고 다음 변환기를 계속합니다. 후크는 표시 전용입니다. 원래 메시지는 세션 및 모델 컨텍스트에서 변경되지 않은 상태로 유지됩니다. 새로운 사용자 메시지, 보조 스트리밍 업데이트, 복원된 세션 메시지 및 터미널 너비 변경에 대해 실행되므로 변환기는 동기식을 유지하고 저렴해야 합니다.\n\n### pi.registerEntryRenderer(customType, 렌더러)\n\n`customType`에 사용자 정의 항목을 위한 사용자 정의 TUI 렌더러를 등록하세요. 사용자 정의 항목은 `pi.appendEntry()`로 생성되며 LLM 컨텍스트에 참여하지 않습니다.\n\n```typescript\nimport { Box, Text } from \"@earendil-works/pi-tui\";\n\npi.registerEntryRenderer(\"status-card\", (entry, { expanded }, theme) => {\n  const data = entry.data as { title: string; count: number };\n  const box = new Box(1, 1, (text) => theme.bg(\"customMessageBg\", text));\n  box.addChild(new Text(`${theme.bold(data.title)}: ${data.count}`));\n  if (expanded) {\n    box.addChild(new Text(theme.fg(\"dim\", JSON.stringify(data, null, 2))));\n  }\n  return box;\n});\n\npi.appendEntry(\"status-card\", { title: \"Indexed files\", count: 17 });\n```\n\n### pi.registerShortcut(단축키, 옵션)\n\n키보드 단축키를 등록하세요. 단축키 형식과 내장된 키 바인딩은 [keybindings.md](keybindings.md)를 참조하세요.\n\n```typescript\npi.registerShortcut(\"ctrl+shift+p\", {\n  description: \"Toggle plan mode\",\n  handler: async (ctx) => {\n    ctx.ui.notify(\"Toggled!\");\n  },\n});\n```\n\n### pi.registerFlag(이름, 옵션)\n\nCLI 플래그를 등록합니다.\n\n```typescript\npi.registerFlag(\"plan\", {\n  description: \"Start in plan mode\",\n  type: \"boolean\",\n  default: false,\n});\n\n// Check value\nif (pi.getFlag(\"plan\")) {\n  // Plan mode enabled\n}\n```\n\n### pi.exec(명령어, 인수, 옵션?)\n\n쉘 명령을 실행합니다.\n\n```typescript\nconst result = await pi.exec(\"git\", [\"status\"], { signal, timeout: 5000 });\n// result.stdout, result.stderr, result.code, result.killed\n```\n\n### pi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(이름)\n\n활성 도구를 관리합니다. 이는 내장 도구와 동적으로 등록된 도구 모두에 적용됩니다. `pi.getActiveTools()`는 활성 도구 이름을 `string[]`로 반환합니다. `pi.getAllTools()`는 구성된 모든 도구에 대한 메타데이터를 반환합니다.\n\n```typescript\nconst active = pi.getActiveTools(); // [\"read\", \"bash\", ...]\nconst all = pi.getAllTools();\n// all = [{\n//   name: \"read\",\n//   description: \"Read file contents...\",\n//   parameters: ...,\n//   promptGuidelines: [\"Use read to examine files instead of cat or sed.\"],\n//   sourceInfo: { path: \"<builtin:read>\", source: \"builtin\", scope: \"temporary\", origin: \"top-level\" }\n// }, ...]\nconst builtinTools = all.filter((t) => t.sourceInfo.source === \"builtin\");\nconst extensionTools = all.filter((t) => t.sourceInfo.source !== \"builtin\" && t.sourceInfo.source !== \"sdk\");\npi.setActiveTools([...new Set([...active, \"my_custom_tool\"])]); // Keep current tools and enable my_custom_tool\npi.setActiveTools([\"read\", \"bash\"]); // Switch to read-only\n```\n\n`pi.getAllTools()`는 `name`, `description`, `parameters`, `promptGuidelines` 및 `sourceInfo`를 반환합니다.\n\n일반적인 `sourceInfo.source` 값:\n- `builtin` 내장 도구\n- `sdk` `createAgentSession({ customTools })`를 통해 전달된 도구의 경우\n- 확장으로 등록된 도구에 대한 확장 소스 메타데이터\n\n### pi.setModel(모델)\n\n현재 모델을 설정합니다. 모델에 API key를 사용할 수 없는 경우 `false`를 반환합니다. 사용자 정의 모델을 구성하려면 [models.md](models.md)를 참조하세요.\n\n```typescript\nconst model = ctx.modelRegistry.find(\"anthropic\", \"claude-sonnet-4-5\");\nif (model) {\n  const success = await pi.setModel(model);\n  if (!success) {\n    ctx.ui.notify(\"No API key for this model\", \"error\");\n  }\n}\n```\n\n### pi.getThinkingLevel() / pi.setThinkingLevel(레벨)\n\n사고 수준을 얻거나 설정하십시오. 수준은 모델 기능에 따라 고정됩니다(비추론 모델은 항상 \"off\"를 사용함). 변경 사항은 `thinking_level_select`를 내보냅니다.\n\n```typescript\nconst current = pi.getThinkingLevel();  // \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\"\npi.setThinkingLevel(\"high\");\n```\n\n### 파이.이벤트\n\n확장 간 통신을 위한 공유 이벤트 버스:\n\n```typescript\npi.events.on(\"my:event\", (data) => { ... });\npi.events.emit(\"my:event\", { ... });\n```\n\n### pi.registerProvider(이름, 구성)\n\n모델 공급자를 동적으로 등록하거나 재정의합니다. 프록시, 사용자 정의 엔드포인트 또는 팀 전체 모델 구성에 유용합니다.\n\n확장 팩토리 기능 중에 이루어진 호출은 대기열에 추가되고 실행기가 초기화되면 적용됩니다. 그 이후에 이루어진 호출(예: 사용자 설정 흐름에 따른 명령 처리기)은 `/reload` 없이도 즉시 적용됩니다.\n\n동적 공급자는 `refreshModels`을 구현할 수 있습니다. Pi는 모델 새로 고침 중에 이를 호출하고, 반환된 목록을 공급자를 통해 동기적으로 게시하고, 정식 자격 증명/저장된 카탈로그/네트워크/신호 컨텍스트를 전달합니다. 확장은 생성 확인 `context.publish({ persist: entry })`을 통해 카탈로그 메타데이터를 유지할지 여부를 결정합니다. llama.cpp와 같은 라이브 서버는 모델을 유지하지 않고 모델을 반환할 수 있습니다.\n\n`context.signal`는 항상 구체적인 신호이며 공급자 콜백은 이를 차단 I/O에 전달해야 합니다. 공개 `ModelRuntime.refresh()` 및 `ModelRegistry.refresh()` 호출은 선택적 신호를 허용하며 생략되면 제한이 없습니다. 확장 프로그램과 응용 프로그램은 자체 마감일을 선택합니다. 취소하면 공급자가 신호를 무시하더라도 발신자가 기다리는 것을 중지하지만 기본 작업을 중지하려면 여전히 협력이 필요합니다.\n\nExtensions 기본 공급자 인증, 필터링, 새로 고침 또는 스트림 동작이 필요한 경우 `@earendil-works/pi-ai`에서 전체 `Provider`를 등록할 수 있습니다. 공급자는 구성 기반이 되며 그 위에 `models.json` 재정의가 계속 적용됩니다.\n\n```typescript\nimport { createProvider, openAICompletionsApi } from \"@earendil-works/pi-ai\";\n\nconst provider = createProvider({\n  id: \"local-server\",\n  name: \"Local Server\",\n  baseUrl: \"http://localhost:8080/v1\",\n  auth: {\n    apiKey: {\n      name: \"Local server setup\",\n      async login(interaction) {\n        return {\n          type: \"api_key\",\n          key: await interaction.prompt({ type: \"secret\", message: \"API key\" }),\n        };\n      },\n      async resolve({ credential }) {\n        return credential?.key\n          ? { auth: { apiKey: credential.key }, source: \"stored API key\" }\n          : undefined;\n      },\n    },\n  },\n  models: [],\n  api: openAICompletionsApi(),\n});\n\npi.registerProvider(provider);\n\n// Register a new provider with custom models\npi.registerProvider(\"my-proxy\", {\n  name: \"My Proxy\",\n  baseUrl: \"https://proxy.example.com\",\n  apiKey: \"$PROXY_API_KEY\",  // env var reference\n  api: \"anthropic-messages\",\n  models: [\n    {\n      id: \"claude-sonnet-4-20250514\",\n      name: \"Claude 4 Sonnet (proxy)\",\n      reasoning: false,\n      input: [\"text\", \"image\"],\n      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n      contextWindow: 200000,\n      maxTokens: 16384\n    }\n  ]\n});\n\n// Register a live llama.cpp catalog without persisting discovered models\npi.registerProvider(\"llama.cpp\", {\n  baseUrl: \"http://localhost:8080/v1\",\n  apiKey: \"local\",\n  api: \"openai-completions\",\n  async refreshModels({ signal }) {\n    const response = await fetch(\"http://localhost:8080/v1/models\", { signal });\n    const { data } = await response.json();\n    return data.map(({ id }) => ({\n      id,\n      name: id,\n      reasoning: false,\n      input: [\"text\"],\n      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n      contextWindow: 128000,\n      maxTokens: 16384\n    }));\n  }\n});\n\n// Override baseUrl for an existing provider (keeps all models)\npi.registerProvider(\"anthropic\", {\n  baseUrl: \"https://proxy.example.com\"\n});\n\n// Register provider with OAuth support for /login\npi.registerProvider(\"corporate-ai\", {\n  baseUrl: \"https://ai.corp.com\",\n  api: \"openai-responses\",\n  models: [...],\n  oauth: {\n    name: \"Corporate AI (SSO)\",\n    async login(callbacks) {\n      // Custom OAuth flow\n      callbacks.onAuth({ url: \"https://sso.corp.com/...\" });\n      const code = await callbacks.onPrompt({ message: \"Enter code:\" });\n      return { refresh: code, access: code, expires: Date.now() + 3600000 };\n    },\n    async refreshToken(credentials, signal) {\n      signal.throwIfAborted();\n      // Refresh logic\n      return credentials;\n    },\n    getApiKey(credentials) {\n      return credentials.access;\n    }\n  }\n});\n```\n\n개체 양식은 기본 `auth`, `getModels`, `refreshModels`, `filterModels`, `stream` 및 `streamSimple` 동작을 포함하여 완전한 pi-ai `Provider`를 허용합니다.\n\n**기존 구성 옵션:**\n- `name` - `/login`와 같은 UI의 공급자 표시 이름입니다.\n- `baseUrl` - API 엔드포인트 URL. 모델을 정의할 때 필요합니다.\n- `apiKey` - API key 리터럴, 환경 보간(`$ENV_VAR` 또는 `${ENV_VAR}`) 또는 선행 `!command`. 모델을 정의할 때 필요합니다(`oauth`가 제공되지 않은 경우). `$`는 ``apiKey` - API key 리터럴, 환경 보간(`$ENV_VAR` 또는 `${ENV_VAR}`) 또는 선행 `!command`. 모델을 정의할 때 필요합니다(`oauth`가 제공되지 않은 경우). `$`는 를 이스케이프하고, `$!`는 명령 실행을 트리거하지 않고 리터럴 `!`을 이스케이프합니다.\n- `api` - API 유형: `\"anthropic-messages\"`, `\"openai-completions\"`, `\"openai-responses\"` 등\n- `headers` - 요청에 포함할 맞춤 헤더입니다.\n- `authHeader` - true인 경우 `Authorization: Bearer` 헤더를 자동으로 추가합니다.\n- `models` - 모델 정의 배열. 제공된 경우 이 공급자의 기존 모델을 모두 바꿉니다. 모델 정의는 `baseUrl`를 설정하여 해당 모델의 공급자 엔드포인트를 재정의할 수 있습니다.\n- `refreshModels` - 비동기 동적 검색 콜백. 반환된 모델은 확장 제공 모델을 대체합니다. `context.stored`에는 지속된 공급자 스냅샷이 포함되어 있습니다. 업데이트된 카탈로그 데이터가 지속되어야 하는 경우에만 생성 확인 `context.publish({ persist: entry })`을 사용하세요. 해당 스냅샷을 삭제하려면 `persist: null`를 사용하세요.\n- `oauth` - `/login` 지원을 위한 OAuth 공급자 구성입니다. 제공되면 공급자가 로그인 메뉴에 나타납니다.\n- `streamSimple` - 비표준 API에 대한 맞춤 스트리밍 구현입니다.\n\n고급 주제는 [custom-provider.md](custom-provider.md)를 참조하세요: 사용자 정의 스트리밍 APIs, OAuth 세부 정보, 모델 정의 참조.\n\n### pi.unregisterProvider(이름)\n\n이전에 등록된 공급자와 해당 모델을 제거합니다. 공급자가 재정의한 기본 제공 모델이 복원됩니다. 공급자가 등록되지 않은 경우 아무런 효과가 없습니다.\n\n`registerProvider`와 마찬가지로 초기 로드 단계 이후에 호출되면 즉시 적용되므로 `/reload`가 필요하지 않습니다.\n\n```typescript\npi.registerCommand(\"my-setup-teardown\", {\n  description: \"Remove the custom proxy provider\",\n  handler: async (_args, _ctx) => {\n    pi.unregisterProvider(\"my-proxy\");\n  },\n});\n```\n\n## 상태 관리\n\nExtensions 상태는 적절한 분기 지원을 위해 도구 결과 `details`에 저장해야 합니다.\n\n```typescript\nexport default function (pi: ExtensionAPI) {\n  let items: string[] = [];\n\n  // Reconstruct state from session\n  pi.on(\"session_start\", async (_event, ctx) => {\n    items = [];\n    for (const entry of ctx.sessionManager.getBranch()) {\n      if (entry.type === \"message\" && entry.message.role === \"toolResult\") {\n        if (entry.message.toolName === \"my_tool\") {\n          items = entry.message.details?.items ?? [];\n        }\n      }\n    }\n  });\n\n  pi.registerTool({\n    name: \"my_tool\",\n    // ...\n    async execute(toolCallId, params, signal, onUpdate, ctx) {\n      items.push(\"new item\");\n      return {\n        content: [{ type: \"text\", text: \"Added\" }],\n        details: { items: [...items] },  // Store for reconstruction\n      };\n    },\n  });\n}\n```\n\n## 맞춤형 도구\n\n`pi.registerTool()`를 통해 LLM이 호출할 수 있는 도구를 등록하세요. 도구는 시스템 프롬프트에 표시되며 사용자 정의 렌더링을 가질 수 있습니다.\n\n기본 시스템 프롬프트의 `Available tools` 섹션에 짧은 한 줄 항목을 입력하려면 `promptSnippet`를 사용하세요. 생략하면 사용자 정의 도구가 해당 섹션에서 제외됩니다.\n\n기본 시스템 프롬프트 `Guidelines` 섹션에 도구별 글머리 기호를 추가하려면 `promptGuidelines`를 사용하세요. 이러한 글머리 기호는 도구가 활성화된 동안에만 포함됩니다(예: `pi.setActiveTools([...])` 이후).\n\n**중요:** `promptGuidelines` 글머리 기호는 도구 이름 접두사 또는 그룹화 없이 `Guidelines` 섹션에 단순하게 추가됩니다. 각 지침은 참조하는 도구의 이름을 지정해야 합니다. LLM은 \"이\"가 어떤 도구를 의미하는지 알 수 없기 때문에 \"...일 때 이 도구를 사용하십시오.\"를 피하십시오. 대신 \"...일 때 my_tool 사용\"이라고 작성하세요.\n\n참고: 일부 모델은 바보이며 도구 경로 인수에 @ 접두사를 포함합니다. 내장 도구는 경로를 확인하기 전에 선행 @를 제거합니다. 사용자 정의 도구가 경로를 허용하는 경우 선행 @도 정규화하세요.\n\n사용자 정의 도구가 파일을 변경하는 경우 `withFileMutationQueue()`를 사용하여 기본 제공 `edit` 및 `write`와 동일한 파일별 대기열에 참여하도록 합니다. 도구 호출은 기본적으로 병렬로 실행되기 때문에 이는 중요합니다. 대기열이 없으면 두 도구가 동일한 이전 파일 내용을 읽고, 서로 다른 업데이트를 계산한 다음, 마지막 쓰기 토지가 다른 도구를 덮어쓸 수 있습니다.\n\n실패 사례 예: 사용자 정의 도구가 내장된 동안 `foo.ts`을 편집하고 `edit`도 동일한 보조 회전에서 `foo.ts`를 변경합니다. 도구가 대기열에 참여하지 않으면 둘 다 원본 `foo.ts`을 읽고 별도의 변경 사항을 적용할 수 있으며 해당 변경 사항 중 하나가 손실됩니다.\n\n원시 사용자 인수가 아닌 실제 대상 파일 경로를 `withFileMutationQueue()`에 전달합니다. 먼저 `ctx.cwd` 또는 도구의 작업 디렉터리를 기준으로 절대 경로로 해결하세요. 기존 파일의 경우 도우미는 `realpath()`를 통해 정규화하므로 동일한 파일에 대한 심볼릭 링크 별칭은 하나의 대기열을 공유합니다. 새 파일의 경우 아직 `realpath()`에 대한 내용이 없기 때문에 확인된 절대 경로로 대체됩니다.\n\n해당 대상 경로에 전체 돌연변이 창을 대기열에 추가합니다. 여기에는 최종 쓰기뿐만 아니라 읽기-수정-쓰기 논리도 포함됩니다.\n\n```typescript\nimport { withFileMutationQueue } from \"@earendil-works/pi-coding-agent\";\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, resolve } from \"node:path\";\n\nasync execute(_toolCallId, params, _signal, _onUpdate, ctx) {\n  const absolutePath = resolve(ctx.cwd, params.path);\n\n  return withFileMutationQueue(absolutePath, async () => {\n    await mkdir(dirname(absolutePath), { recursive: true });\n    const current = await readFile(absolutePath, \"utf8\");\n    const next = current.replace(params.oldText, params.newText);\n    await writeFile(absolutePath, next, \"utf8\");\n\n    return {\n      content: [{ type: \"text\", text: `Updated ${params.path}` }],\n      details: {},\n    };\n  });\n}\n```\n\n### 도구 정의\n\n```typescript\nimport { Type } from \"typebox\";\nimport { StringEnum } from \"@earendil-works/pi-ai\";\nimport { Text } from \"@earendil-works/pi-tui\";\n\npi.registerTool({\n  name: \"my_tool\",\n  label: \"My Tool\",\n  description: \"What this tool does (shown to LLM)\",\n  promptSnippet: \"List or add items in the project todo list\",\n  promptGuidelines: [\n    \"Use my_tool for todo planning instead of direct file edits when the user asks for a task list.\"\n  ],\n  parameters: Type.Object({\n    action: StringEnum([\"list\", \"add\"] as const),  // Use StringEnum for Google compatibility\n    text: Type.Optional(Type.String()),\n  }),\n  prepareArguments(args) {\n    if (!args || typeof args !== \"object\") return args;\n    const input = args as { action?: string; oldAction?: string };\n    if (typeof input.oldAction === \"string\" && input.action === undefined) {\n      return { ...input, action: input.oldAction };\n    }\n    return args;\n  },\n\n  async execute(toolCallId, params, signal, onUpdate, ctx) {\n    // Check for cancellation\n    if (signal?.aborted) {\n      return { content: [{ type: \"text\", text: \"Cancelled\" }] };\n    }\n\n    // Stream progress updates\n    onUpdate?.({\n      content: [{ type: \"text\", text: \"Working...\" }],\n      details: { progress: 50 },\n    });\n\n    // Run commands via pi.exec (captured from extension closure)\n    const result = await pi.exec(\"some-command\", [], { signal });\n\n    // Return result\n    return {\n      content: [{ type: \"text\", text: \"Done\" }],  // Sent to LLM\n      details: { data: result },                   // For rendering & state\n      // usage: nestedModelResponse.usage,          // Optional nested LLM usage\n      // Optional: stop after this tool batch when every finalized tool result\n      // in the batch also returns terminate: true.\n      terminate: true,\n    };\n  },\n\n  // Optional: Custom rendering\n  renderCall(args, theme, context) { ... },\n  renderResult(result, options, theme, context) { ... },\n});\n```\n\n**사용 계산:** 도구가 중첩된 LLM 호출을 수행하는 경우 결합된 `Usage`를 `usage`로 반환합니다. Pi는 도구 결과에 이를 유지하고 바닥글, `/session` 및 RPC 세션 총계에 포함합니다. `tool_result` 핸들러는 이 값을 검사하거나 바꿀 수 있습니다.\n\n**신호 오류:** 도구 실행을 실패로 표시하려면(결과에 `isError: true`을 설정하고 LLM에 보고) `execute`에서 오류를 발생시킵니다. 값을 반환하면 반환 개체에 포함된 속성에 관계없이 오류 플래그가 설정되지 않습니다.\n\n**조기 종료:** `execute()`에서 `terminate: true`를 반환하여 현재 도구 배치 후에 자동 후속 LLM 호출을 건너뛰어야 함을 암시합니다. 이는 해당 배치의 모든 최종 도구 결과가 종료될 때만 적용됩니다. 에이전트가 최종 구조화된 출력 도구 호출로 끝나는 최소 예는 [examples/extensions/structured-output.ts](../examples/extensions/structured-output.ts)를 참조하세요.\n\n```typescript\n// Correct: throw to signal an error\nasync execute(toolCallId, params) {\n  if (!isValid(params.input)) {\n    throw new Error(`Invalid input: ${params.input}`);\n  }\n  return { content: [{ type: \"text\", text: \"OK\" }], details: {} };\n}\n```\n\n**중요:** 문자열 열거형에는 `@earendil-works/pi-ai`에서 `StringEnum`를 사용하세요. `Type.Union`/`Type.Literal`는 Google의 API와 작동하지 않습니다.\n\n**인수 준비:** `prepareArguments(args)`는 선택 사항입니다. 정의된 경우 스키마 유효성 검사 이전과 `execute()` 이전에 실행됩니다. pi가 저장된 도구 호출 인수가 더 이상 현재 스키마와 일치하지 않는 이전 세션을 재개할 때 이를 사용하여 이전에 허용된 입력 형태를 모방합니다. `parameters`에 대해 유효성을 검사하려는 개체를 반환합니다. 공개 스키마를 엄격하게 유지하세요. 이전에 재개된 세션이 계속 작동하도록 하기 위해 더 이상 사용되지 않는 호환성 필드를 `parameters`에 추가하지 마세요.\n\n예: 이전 세션에는 최상위 `oldText` 및 `newText`가 포함된 `edit` 도구 호출이 포함될 수 있지만 현재 스키마는 `edits: [{ oldText, newText }]`만 허용합니다.\n\n```typescript\npi.registerTool({\n  name: \"edit\",\n  label: \"Edit\",\n  description: \"Edit a single file using exact text replacement\",\n  parameters: Type.Object({\n    path: Type.String(),\n    edits: Type.Array(\n      Type.Object({\n        oldText: Type.String(),\n        newText: Type.String(),\n      }),\n    ),\n  }),\n  prepareArguments(args) {\n    if (!args || typeof args !== \"object\") return args;\n\n    const input = args as {\n      path?: string;\n      edits?: Array<{ oldText: string; newText: string }>;\n      oldText?: unknown;\n      newText?: unknown;\n    };\n\n    if (typeof input.oldText !== \"string\" || typeof input.newText !== \"string\") {\n      return args;\n    }\n\n    return {\n      ...input,\n      edits: [...(input.edits ?? []), { oldText: input.oldText, newText: input.newText }],\n    };\n  },\n  async execute(toolCallId, params, signal, onUpdate, ctx) {\n    // params now matches the current schema\n    return {\n      content: [{ type: \"text\", text: `Applying ${params.edits.length} edit block(s)` }],\n      details: {},\n    };\n  },\n});\n```\n\n### 내장 도구 재정의\n\nExtensions는 동일한 이름의 도구를 등록하여 내장 도구(`read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`)를 재정의할 수 있습니다. 이런 일이 발생하면 대화형 모드에서 경고를 표시합니다.\n\n```bash\n# Extension's read tool replaces built-in read\npi -e ./tool-override.ts\n```\n\n또는 `--no-builtin-tools`를 사용하여 확장 도구를 활성화한 상태에서 기본 제공 도구 없이 시작하세요.\n```bash\n# No built-in tools, only extension tools\npi --no-builtin-tools -e ./my-extension.ts\n```\n\n로깅 및 액세스 제어로 `read`를 재정의하는 전체 예는 [examples/extensions/tool-override.ts](../examples/extensions/tool-override.ts)를 참조하세요.\n\n**렌더링:** 내장 렌더러 상속은 슬롯별로 해결됩니다. 실행 재정의와 렌더링 재정의는 독립적입니다. 재정의에서 `renderCall`가 생략되면 내장된 `renderCall`가 사용됩니다. 재정의에서 `renderResult`를 생략하면 내장된 `renderResult`가 사용됩니다. 재정의에서 두 가지를 모두 생략하면 내장 렌더러가 자동으로 사용됩니다(구문 강조 표시, diff 등). 이를 통해 UI를 다시 구현하지 않고도 로깅 또는 액세스 제어를 위한 내장 도구를 래핑할 수 있습니다.\n\n**프롬프트 메타데이터:** `promptSnippet` 및 `promptGuidelines`는 내장 도구에서 상속되지 않습니다. 재정의에서 해당 프롬프트 지침을 유지해야 하는 경우 재정의에 명시적으로 정의하세요.\n\n**구현은 `details` 유형을 포함하여 정확한 결과 형태**와 일치해야 합니다. UI 및 세션 논리는 렌더링 및 상태 추적을 위해 이러한 모양에 따라 달라집니다.\n\n내장 도구 구현:\n- [read.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/read.ts) - `ReadToolDetails`\n- [bash.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/bash.ts) - `BashToolDetails`\n- [edit.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/edit.ts)\n- [write.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/write.ts)\n- [grep.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/grep.ts) - `GrepToolDetails`\n- [find.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/find.ts) - `FindToolDetails`\n- [ls.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/ls.ts) - `LsToolDetails`\n\n### 원격 실행\n\n내장된 도구는 원격 시스템(SSH, 컨테이너 등)에 위임하기 위한 플러그형 작업을 지원합니다.\n\n```typescript\nimport { createReadTool, createBashTool, type ReadOperations } from \"@earendil-works/pi-coding-agent\";\n\n// Create tool with custom operations\nconst remoteRead = createReadTool(cwd, {\n  operations: {\n    readFile: (path) => sshExec(remote, `cat ${path}`),\n    access: (path) => sshExec(remote, `test -r ${path}`).then(() => {}),\n  }\n});\n\n// Register, checking flag at execution time\npi.registerTool({\n  ...remoteRead,\n  async execute(id, params, signal, onUpdate, _ctx) {\n    const ssh = getSshConfig();\n    if (ssh) {\n      const tool = createReadTool(cwd, { operations: createRemoteOps(ssh) });\n      return tool.execute(id, params, signal, onUpdate);\n    }\n    return localRead.execute(id, params, signal, onUpdate);\n  },\n});\n```\n\n**작업 인터페이스:** `ReadOperations`, `WriteOperations`, `EditOperations`, `BashOperations`, `LsOperations`, `GrepOperations`, `FindOperations`\n\n`user_bash`의 경우 확장 프로그램은 로컬 프로세스 생성, 셸 확인 및 프로세스 트리 종료를 다시 구현하는 대신 `createLocalBashOperations()`를 통해 pi의 로컬 셸 백엔드를 재사용할 수 있습니다.\n\nbash 도구는 실행 전에 명령, cwd 또는 env를 조정하기 위한 생성 후크도 지원합니다.\n\n```typescript\nimport { createBashTool } from \"@earendil-works/pi-coding-agent\";\n\nconst bashTool = createBashTool(cwd, {\n  spawnHook: ({ command, cwd, env }) => ({\n    command: `source ~/.profile\\n${command}`,\n    cwd: `/mnt/sandbox${cwd}`,\n    env: { ...env, CI: \"1\" },\n  }),\n});\n```\n\n`createBashTool()`는 `PI_SESSION_ID`, `PI_SESSION_FILE`, `PI_PROVIDER`, `PI_MODEL` 및 `PI_REASONING_LEVEL`를 통해 명령에 현재 세션을 노출합니다. 주입은 `spawnHook` 이전에 발생하므로 후크는 `env`에서 이러한 값을 수신하고 위와 같이 기존 환경을 확산할 때 이를 보존합니다. 비활성화하려면 `exposeSessionEnvironment: false`를 설정하세요.\n\n```typescript\nconst bashTool = createBashTool(cwd, {\n  exposeSessionEnvironment: false,\n});\n```\n\n변수 의미는 [Bash tool session environment](environment-variables.md#bash-tool-session-environment)를 참조하세요. `--ssh` 플래그가 포함된 전체 SSH 예시는 [examples/extensions/ssh.ts](../examples/extensions/ssh.ts)를 참조하세요.\n\n### 출력 잘림\n\n**도구는 LLM 컨텍스트를 압도하지 않도록 출력을 잘라야 합니다**. 출력이 크면 다음이 발생할 수 있습니다.\n- 컨텍스트 오버플로 오류(프롬프트가 너무 김)\n- 압축 실패\n- 저하된 모델 성능\n\n기본 제공 제한은 **50KB**(~10,000개 토큰) 및 **2000줄** 중 먼저 도달하는 것입니다. 내보낸 자르기 유틸리티를 사용합니다.\n\n```typescript\nimport {\n  truncateHead,      // Keep first N lines/bytes (good for file reads, search results)\n  truncateTail,      // Keep last N lines/bytes (good for logs, command output)\n  truncateLine,      // Truncate a single line to maxBytes with ellipsis\n  formatSize,        // Human-readable size (e.g., \"50KB\", \"1.5MB\")\n  DEFAULT_MAX_BYTES, // 50KB\n  DEFAULT_MAX_LINES, // 2000\n} from \"@earendil-works/pi-coding-agent\";\n\nasync execute(toolCallId, params, signal, onUpdate, ctx) {\n  const output = await runCommand();\n\n  // Apply truncation\n  const truncation = truncateHead(output, {\n    maxLines: DEFAULT_MAX_LINES,\n    maxBytes: DEFAULT_MAX_BYTES,\n  });\n\n  let result = truncation.content;\n\n  if (truncation.truncated) {\n    // Write full output to temp file\n    const tempFile = writeTempFile(output);\n\n    // Inform the LLM where to find complete output\n    result += `\\n\\n[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines`;\n    result += ` (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}).`;\n    result += ` Full output saved to: ${tempFile}]`;\n  }\n\n  return { content: [{ type: \"text\", text: result }] };\n}\n```\n\n**핵심 사항:**\n- 시작이 중요한 콘텐츠(검색 결과, 파일 읽기)에는 `truncateHead`를 사용하세요.\n- 끝이 중요한 콘텐츠(로그, 명령 출력)에는 `truncateTail`를 사용하세요.\n- 출력이 잘린 경우 항상 LLM에 알리고 전체 버전을 찾을 수 있는 위치를 알려주세요.\n- 도구 설명에 잘림 제한을 문서화하세요.\n\n적절한 잘림으로 `rg`(ripgrep)을 래핑하는 전체 예제는 [examples/extensions/truncated-tool.ts](../examples/extensions/truncated-tool.ts)를 참조하세요.\n\n### 여러 도구\n\n하나의 확장은 공유 상태로 여러 도구를 등록할 수 있습니다.\n\n```typescript\nexport default function (pi: ExtensionAPI) {\n  let connection = null;\n\n  pi.registerTool({ name: \"db_connect\", ... });\n  pi.registerTool({ name: \"db_query\", ... });\n  pi.registerTool({ name: \"db_close\", ... });\n\n  pi.on(\"session_shutdown\", async () => {\n    connection?.close();\n  });\n}\n```\n\n### 맞춤형 렌더링\n\n도구는 사용자 정의 TUI 디스플레이를 위해 `renderCall` 및 `renderResult`를 제공할 수 있습니다. 전체 구성 요소는 [tui.md](tui.md)를, 도구 행 구성 방법은 API를, [tool-execution.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/modes/interactive/components/tool-execution.ts)를 참조하세요.\n\n기본적으로 도구 출력은 패딩과 배경을 처리하는 `Box`로 래핑됩니다. 정의된 `renderCall` 또는 `renderResult`는 `Component`를 반환해야 합니다. 슬롯 렌더러가 정의되지 않은 경우 `tool-execution.ts`는 해당 슬롯에 대해 대체 렌더링을 사용합니다.\n\n도구가 기본 `Box`를 사용하는 대신 자체 셸을 렌더링해야 하는 경우 `renderShell: \"self\"`를 설정하세요. 이는 프레임이나 배경 동작을 완벽하게 제어해야 하는 도구에 유용합니다. 예를 들어 도구가 안정된 후에도 시각적으로 안정적인 상태를 유지해야 하는 대규모 미리 보기입니다.\n\n```typescript\npi.registerTool({\n  name: \"my_tool\",\n  label: \"My Tool\",\n  description: \"Custom shell example\",\n  parameters: Type.Object({}),\n  renderShell: \"self\",\n  async execute() {\n    return { content: [{ type: \"text\", text: \"ok\" }], details: undefined };\n  },\n  renderCall(args, theme, context) {\n    return new Text(theme.fg(\"accent\", \"my custom shell\"), 0, 0);\n  },\n});\n```\n\n`renderCall` 및 `renderResult`는 각각 다음을 포함하는 `context` 객체를 받습니다.\n- `args` - 현재 도구 호출 인수\n- `state` - `renderCall` 및 `renderResult`에서 공유 행-로컬 상태\n- `lastComponent` - 해당 슬롯에 대해 이전에 반환된 구성요소(있는 경우)\n- `invalidate()` - 이 도구 행의 다시 렌더링을 요청합니다.\n- `toolCallId`, `cwd`, `executionStarted`, `argsComplete`, `isPartial`, `expanded`, `showImages`, `isError`\n\n교차 슬롯 공유 상태에는 `context.state`를 사용하세요. 렌더링 전체에서 동일한 구성 요소를 재사용하고 변경하려는 경우 반환된 구성 요소 인스턴스에 슬롯 로컬 캐시를 유지하세요.\n\n#### 렌더콜\n\n도구 호출 또는 헤더를 렌더링합니다.\n\n```typescript\nimport { Text } from \"@earendil-works/pi-tui\";\n\nrenderCall(args, theme, context) {\n  const text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n  let content = theme.fg(\"toolTitle\", theme.bold(\"my_tool \"));\n  content += theme.fg(\"muted\", args.action);\n  if (args.text) {\n    content += \" \" + theme.fg(\"dim\", `\"${args.text}\"`);\n  }\n  text.setText(content);\n  return text;\n}\n```\n\n#### 렌더링 결과\n\n도구 결과 또는 출력을 렌더링합니다.\n\n```typescript\nrenderResult(result, { expanded, isPartial }, theme, context) {\n  if (isPartial) {\n    return new Text(theme.fg(\"warning\", \"Processing...\"), 0, 0);\n  }\n\n  if (result.details?.error) {\n    return new Text(theme.fg(\"error\", `Error: ${result.details.error}`), 0, 0);\n  }\n\n  let text = theme.fg(\"success\", \"✓ Done\");\n  if (expanded && result.details?.items) {\n    for (const item of result.details.items) {\n      text += \"\\n  \" + theme.fg(\"dim\", item);\n    }\n  }\n  return new Text(text, 0, 0);\n}\n```\n\n슬롯에 의도적으로 표시되는 콘텐츠가 없는 경우 빈 `Container`와 같은 빈 `Component`를 반환합니다.\n\n#### 키바인딩 힌트\n\n활성 키 바인딩 구성을 존중하는 키 바인딩 힌트를 표시하려면 `keyHint()`를 사용하세요.\n\n```typescript\nimport { keyHint } from \"@earendil-works/pi-coding-agent\";\n\nrenderResult(result, { expanded }, theme, context) {\n  let text = theme.fg(\"success\", \"✓ Done\");\n  if (!expanded) {\n    text += ` (${keyHint(\"app.tools.expand\", \"to expand\")})`;\n  }\n  return new Text(text, 0, 0);\n}\n```\n\n사용 가능한 기능:\n- `keyHint(keybinding, description)` - `\"app.tools.expand\"` 또는 `\"tui.select.confirm\"`와 같이 구성된 키 바인딩 ID의 형식을 지정합니다.\n- `keyText(keybinding)` - 키 바인딩 ID에 대해 구성된 원시 키 텍스트를 반환합니다.\n- `rawKeyHint(key, description)` - 원시 키 문자열 형식 지정\n\n네임스페이스가 있는 키 바인딩 ID를 사용하세요.\n- 코딩 에이전트 ID는 `app.*` 네임스페이스를 사용합니다(예: `app.tools.expand`, `app.editor.external`, `app.session.rename`).\n- 공유 TUI ID는 `tui.*` 네임스페이스를 사용합니다(예: `tui.select.confirm`, `tui.select.cancel`, `tui.input.tab`).\n\n키 바인딩 ID 및 기본값의 전체 목록은 [keybindings.md](keybindings.md)를 참조하세요. `keybindings.json`는 동일한 네임스페이스 ID를 사용합니다.\n\n사용자 정의 편집기와 `ctx.ui.custom()` 구성 요소는 삽입된 인수로 `keybindings: KeybindingsManager`를 받습니다. `getKeybindings()` 또는 `setKeybindings()`를 호출하는 대신 삽입된 관리자를 직접 사용해야 합니다.\n\n#### 모범 사례\n\n- `Text`를 패딩 `(0, 0)`과 함께 사용하세요. 기본 Box는 패딩을 처리합니다.\n- 여러 줄로 구성된 콘텐츠에는 `\\n`를 사용하세요.\n- 스트리밍 진행을 위해 `isPartial`를 처리합니다.\n- 자세한 내용은 요청 시 `expanded`로 문의하세요.\n- 기본 보기를 컴팩트하게 유지하세요.\n- 인수를 `context.state`에 복사하는 대신 `renderResult`에서 `context.args`를 읽으세요.\n- 호출 및 결과 슬롯 전체에서 공유되어야 하는 데이터에만 `context.state`를 사용하세요.\n- 동일한 구성요소 인스턴스를 업데이트할 수 있는 경우 `context.lastComponent`를 재사용하세요.\n- 기본 박스형 셸이 방해가 되는 경우에만 `renderShell: \"self\"`를 사용하세요. 자체 셸 모드에서는 도구가 자체 프레임, 패딩 및 배경을 담당합니다.\n\n#### 대체\n\n슬롯 렌더러가 정의되지 않았거나 발생하는 경우:\n- `renderCall`: 도구 이름을 표시합니다.\n- `renderResult`: `content`의 원시 텍스트를 표시합니다.\n\n### 동적 도구 로딩\n\nExtensions 작은 초기 세트만 활성화하면서 많은 도구를 등록할 수 있습니다. 그런 다음 도구는 실행 중에 `pi.setActiveTools()`를 사용하여 더 많은 도구를 추가할 수 있습니다. Pi 순전히 추가된 변경 사항을 감지하고 해당 도구 결과에 새로 사용 가능한 도구 이름을 기록하며 다음 모델 요청 전에 업데이트된 활성 세트를 적용합니다.\n\n이것은 모든 모델에서 작동합니다. Models 기본 지연 로딩 지원을 통해 안정적인 프롬프트 접두사를 유지하고 도구 결과 위치에 새 정의를 로드합니다. 다른 모델은 아래 설명된 대체 방법을 사용합니다.\n\n수명주기는 다음과 같습니다\n\n1. 모든 도구를 `pi.registerTool()`로 등록하면 `pi.getAllTools()`에 표시됩니다.\n2. `search_tools`와 같은 로더 도구는 활성 상태로 유지하고 검색 가능한 도구는 비활성 상태로 둡니다.\n3. 로더 실행 중에 `pi.setActiveTools([...currentTools,...matchingTools])`를 호출하세요. 변경 사항은 추가되어야 합니다. 동일한 호출에서 현재 활성 도구를 제거하지 마십시오.\n4. Pi 로더의 도구 결과에 어떤 도구가 추가되었는지 기록합니다.\n5. 다음 모델 응답 전에 Pi는 지원되는 경우 기본 지연 로딩을 사용하고 그렇지 않은 경우 일반 활성 도구 목록을 사용하여 추가된 정의를 노출합니다.\n\n공급자별 도구 참조를 반환하거나 로더를 특수 검색 도구로 표시할 필요가 없습니다. 활성 공구 교환이 신호입니다. `pi.setActiveTools()`에 전달된 이름은 이미 등록되어 있어야 합니다. 알 수 없는 이름은 무시됩니다.\n\n#### Models 기본 지연 로딩 포함\n\n- **인류학**\n  - **Models:** Sonnet, Opus, Fable 버전 4.5 이상(Haiku 제외)\n  - **기본 표현:** 지연된 정의는 `defer_loading`를 사용합니다. 로드 포인트는 `tool_reference` 콘텐츠를 사용합니다.\n- **오픈AI**\n  - **Models:** `gpt-5.4` 이상 가족\n  - **기본 표현:** Pi 로드 지점에 완성된 클라이언트 `tool_search_call` 및 `tool_search_output` 항목을 추가합니다.\n\n검증된 사용자 정의 모델 또는 프록시의 경우 `anthropic-messages`의 경우 `compat.supportsToolReferences: true`, `openai-responses` 및 `openai-codex-responses`의 경우 `compat.supportsToolSearch: true`를 사용하여 기본 처리를 활성화할 수 있습니다. 엔드포인트와 모델이 해당 기본 프로토콜을 수락하지 않는 한 이를 비활성화된 상태로 둡니다.\n\n#### 대체 동작\n\n다른 모든 모델 및 공급자의 경우 동적 활성화가 계속 작동합니다. Pi는 다음 요청 시 일반적으로 전체 현재 활성 도구 목록을 보냅니다. 모델은 새로 활성화된 도구를 호출할 수 있지만 해당 정의를 추가하면 공급자의 캐시된 프롬프트 접두사가 무효화될 수 있습니다.\n\nPi 또한 한 도구 그룹을 다른 도구 그룹으로 교체하는 등 활성 세트가 순전히 추가되지 않는 경우에도 이 안전한 대체를 사용합니다. 따라서 도구 제거는 작동하지만 지연 로드를 사용하지 않습니다.\n\n최상의 캐시 동작을 위해서는 전체 세션 동안 로더 도구를 활성 상태로 유지하고 활성 세트를 교체하는 대신 도구를 추가하십시오. 또한 `promptSnippet` 또는 `promptGuidelines`를 사용하여 도구를 활성화하면 시스템 프롬프트가 다시 작성됩니다. 공급자가 지연된 스키마를 지원하는 경우에도 시스템 프롬프트 변경으로 인해 접두사가 무효화될 수 있습니다. 느리게 로드된 도구는 일반적으로 도구 `description`에 의존해야 하며 활성 전용 프롬프트 메타데이터를 생략해야 합니다.\n\n#### 검색 도구 예\n\n다음 확장은 두 개의 검색 가능한 도구를 등록하고 초기 활성 세트에서 제거하며 `search_tools`만 로더로 유지합니다. 이 예에서는 간단한 키워드 일치를 사용하지만 검색 구현에서는 BM25, 임베딩, 원격 카탈로그 또는 프로젝트별 라우팅을 사용할 수 있습니다.\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { Type } from \"typebox\";\n\nconst SEARCHABLE_TOOL_NAMES = new Set([\"lookup_weather\", \"search_issues\"]);\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerTool({\n    name: \"lookup_weather\",\n    label: \"Lookup Weather\",\n    description: \"Look up the current weather for a city\",\n    parameters: Type.Object({ city: Type.String() }),\n    async execute(_toolCallId, params) {\n      return {\n        content: [{ type: \"text\", text: `Weather for ${params.city}: sunny` }],\n        details: {},\n      };\n    },\n  });\n\n  pi.registerTool({\n    name: \"search_issues\",\n    label: \"Search Issues\",\n    description: \"Search project issues by keyword\",\n    parameters: Type.Object({ query: Type.String() }),\n    async execute(_toolCallId, params) {\n      return {\n        content: [{ type: \"text\", text: `No open issues matching ${params.query}` }],\n        details: {},\n      };\n    },\n  });\n\n  pi.registerTool({\n    name: \"search_tools\",\n    label: \"Search Tools\",\n    description: \"Search for and enable tools relevant to a task\",\n    promptSnippet: \"Search for additional tools when the active tools cannot perform the task\",\n    promptGuidelines: [\n      \"Use search_tools when a task requires a capability that is not currently available.\",\n    ],\n    parameters: Type.Object({\n      query: Type.String({ description: \"Capability or task to search for\" }),\n      limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 10 })),\n    }),\n    async execute(_toolCallId, params) {\n      const terms = params.query.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);\n      const matches = pi.getAllTools()\n        .filter((tool) => SEARCHABLE_TOOL_NAMES.has(tool.name))\n        .map((tool) => ({\n          tool,\n          score: terms.reduce(\n            (score, term) =>\n              score + (`${tool.name} ${tool.description}`.toLowerCase().includes(term) ? 1 : 0),\n            0,\n          ),\n        }))\n        .filter((match) => match.score > 0)\n        .sort((a, b) => b.score - a.score)\n        .slice(0, params.limit ?? 3)\n        .map((match) => match.tool.name);\n\n      if (matches.length === 0) {\n        return {\n          content: [{ type: \"text\", text: `No tools found for: ${params.query}` }],\n          details: { matches: [] },\n        };\n      }\n\n      const active = pi.getActiveTools();\n      const added = matches.filter((name) => !active.includes(name));\n      pi.setActiveTools([...new Set([...active, ...added])]);\n\n      return {\n        content: [{\n          type: \"text\",\n          text: added.length > 0\n            ? `Loaded tools: ${added.join(\", \")}`\n            : `Matching tools already active: ${matches.join(\", \")}`,\n        }],\n        details: { matches, added },\n      };\n    },\n  });\n\n  pi.on(\"session_start\", () => {\n    // Keep searchable tools registered but initially inactive. Preserve built-ins\n    // and tools owned by other extensions, and keep the loader itself active.\n    const initialTools = pi.getActiveTools().filter(\n      (name) => !SEARCHABLE_TOOL_NAMES.has(name),\n    );\n    pi.setActiveTools([...new Set([...initialTools, \"search_tools\"])]);\n  });\n}\n```\n\n`search_tools`가 일치 항목을 추가하면 모델은 바로 다음 요청에서 해당 정의를 받습니다. 기본 지원 모델에서 정의는 초기 도구 스키마 접두사를 변경하지 않고 검색 결과 뒤에 고정됩니다. 다른 모델에서는 동일한 후속 요청의 일반 도구 목록에 나타납니다.\n\n## 커스텀 UI\n\nExtensions는 `ctx.ui` 메소드를 통해 사용자와 상호작용하고 메시지/도구가 렌더링되는 방식을 맞춤설정할 수 있습니다.\n\n**맞춤 구성요소의 경우 다음에 대한 복사-붙여넣기 패턴이 있는 [tui.md](tui.md)**를 참조하세요.\n- 선택 대화 상자(SelectList)\n- 취소를 사용한 비동기 작업(BorderedLoader)\n- 설정 토글(SettingsList)\n- 상태 표시기(setStatus)\n- 스트리밍 중 작동하는 메시지, 가시성 및 표시기(`setWorkingMessage`, `setWorkingVisible`, `setWorkingIndicator`)\n- 편집기 위/아래 위젯(setWidget)\n- 내장된 슬래시/경로 완성 위에 계층화된 자동 완성 제공자(addAutocompleteProvider)\n- 맞춤 바닥글(setFooter)\n\n### 대화상자\n\n```typescript\n// Select from options\nconst choice = await ctx.ui.select(\"Pick one:\", [\"A\", \"B\", \"C\"]);\n\n// Confirm dialog\nconst ok = await ctx.ui.confirm(\"Delete?\", \"This cannot be undone\");\n\n// Text input\nconst name = await ctx.ui.input(\"Name:\", \"placeholder\");\n\n// Multi-line editor\nconst text = await ctx.ui.editor(\"Edit:\", \"prefilled text\");\n\n// Notification (non-blocking)\nctx.ui.notify(\"Done!\", \"info\");  // \"info\" | \"warning\" | \"error\"\n```\n\n#### 카운트다운이 포함된 시간 제한 대화\n\n대화상자는 실시간 카운트다운 표시와 함께 자동으로 닫히는 `timeout` 옵션을 지원합니다.\n\n```typescript\n// Dialog shows \"Title (5s)\" → \"Title (4s)\" → ... → auto-dismisses at 0\nconst confirmed = await ctx.ui.confirm(\n  \"Timed Confirmation\",\n  \"This dialog will auto-cancel in 5 seconds. Confirm?\",\n  { timeout: 5000 }\n);\n\nif (confirmed) {\n  // User confirmed\n} else {\n  // User cancelled or timed out\n}\n```\n\n**시간 초과 시 반환 값:**\n- `select()` 반환 `undefined`\n- `confirm()` 반환 `false`\n- `input()` 반환 `undefined`\n\n#### AbortSignal을 사용한 수동 해제\n\n더 세밀하게 제어하려면(예: 시간 초과와 사용자 취소를 구별하려면) `AbortSignal`를 사용하세요.\n\n```typescript\nconst controller = new AbortController();\nconst timeoutId = setTimeout(() => controller.abort(), 5000);\n\nconst confirmed = await ctx.ui.confirm(\n  \"Timed Confirmation\",\n  \"This dialog will auto-cancel in 5 seconds. Confirm?\",\n  { signal: controller.signal }\n);\n\nclearTimeout(timeoutId);\n\nif (confirmed) {\n  // User confirmed\n} else if (controller.signal.aborted) {\n  // Dialog timed out\n} else {\n  // User cancelled (pressed Escape or selected \"No\")\n}\n```\n\n전체 예시는 [examples/extensions/timed-confirm.ts](../examples/extensions/timed-confirm.ts)를 참조하세요.\n\n### 위젯, 상태 및 바닥글\n\n```typescript\n// Status in footer (persistent until cleared)\nctx.ui.setStatus(\"my-ext\", \"Processing...\");\nctx.ui.setStatus(\"my-ext\", undefined);  // Clear\n\n// Working loader (shown during streaming)\nctx.ui.setWorkingMessage(\"Thinking deeply...\");\nctx.ui.setWorkingMessage();  // Restore default\nctx.ui.setWorkingVisible(false);  // Hide the built-in working loader row entirely\nctx.ui.setWorkingVisible(true);   // Show the built-in working loader row\n\n// Working indicator (shown during streaming)\nctx.ui.setWorkingIndicator({ frames: [ctx.ui.theme.fg(\"accent\", \"●\")] });  // Static dot\nctx.ui.setWorkingIndicator({\n  frames: [\n    ctx.ui.theme.fg(\"dim\", \"·\"),\n    ctx.ui.theme.fg(\"muted\", \"•\"),\n    ctx.ui.theme.fg(\"accent\", \"●\"),\n    ctx.ui.theme.fg(\"muted\", \"•\"),\n  ],\n  intervalMs: 120,\n});\nctx.ui.setWorkingIndicator({ frames: [] });  // Hide indicator\nctx.ui.setWorkingIndicator();  // Restore default spinner\n\n// Widget above editor (default)\nctx.ui.setWidget(\"my-widget\", [\"Line 1\", \"Line 2\"]);\n// Widget below editor\nctx.ui.setWidget(\"my-widget\", [\"Line 1\", \"Line 2\"], { placement: \"belowEditor\" });\nctx.ui.setWidget(\"my-widget\", (tui, theme) => new Text(theme.fg(\"accent\", \"Custom\"), 0, 0));\nctx.ui.setWidget(\"my-widget\", undefined);  // Clear\n\n// Custom footer (replaces built-in footer entirely)\nctx.ui.setFooter((tui, theme) => ({\n  render(width) { return [theme.fg(\"dim\", \"Custom footer\")]; },\n  invalidate() {},\n}));\nctx.ui.setFooter(undefined);  // Restore built-in footer\n\n// Terminal title\nctx.ui.setTitle(\"pi - my-project\");\n\n// Editor text\nctx.ui.setEditorText(\"Prefill text\");\nconst current = ctx.ui.getEditorText();\n\n// Paste into editor (triggers paste handling, including collapse for large content)\nctx.ui.pasteToEditor(\"pasted content\");\n\n// Stack custom autocomplete behavior on top of the built-in provider\nctx.ui.addAutocompleteProvider((current) => ({\n  triggerCharacters: [\"#\"],\n  async getSuggestions(lines, line, col, options) {\n    const beforeCursor = (lines[line] ?? \"\").slice(0, col);\n    const match = beforeCursor.match(/(?:^|[ \\t])#([^\\s#]*)$/);\n    if (!match) {\n      return current.getSuggestions(lines, line, col, options);\n    }\n\n    return {\n      prefix: `#${match[1] ?? \"\"}`,\n      items: [{ value: \"#2983\", label: \"#2983\", description: \"Extension API for autocomplete\" }],\n    };\n  },\n  applyCompletion(lines, line, col, item, prefix) {\n    return current.applyCompletion(lines, line, col, item, prefix);\n  },\n  shouldTriggerFileCompletion(lines, line, col) {\n    return current.shouldTriggerFileCompletion?.(lines, line, col) ?? true;\n  },\n}));\n\n// Tool output expansion\nconst wasExpanded = ctx.ui.getToolsExpanded();\nctx.ui.setToolsExpanded(true);\nctx.ui.setToolsExpanded(wasExpanded);\n\n// Custom editor (vim mode, emacs mode, etc.)\nctx.ui.setEditorComponent((tui, theme, keybindings) => new VimEditor(tui, theme, keybindings));\nconst currentEditor = ctx.ui.getEditorComponent();\nctx.ui.setEditorComponent((tui, theme, keybindings) =>\n  new WrappedEditor(tui, theme, keybindings, currentEditor?.(tui, theme, keybindings))\n);\nctx.ui.setEditorComponent(undefined);  // Restore default editor\n\n// Theme management (see themes.md for creating themes)\nconst themes = ctx.ui.getAllThemes();  // [{ name: \"dark\", path: \"/...\" | undefined }, ...]\nconst lightTheme = ctx.ui.getTheme(\"light\");  // Load without switching\nconst result = ctx.ui.setTheme(\"light\");  // Switch by name\nif (!result.success) {\n  ctx.ui.notify(`Failed: ${result.error}`, \"error\");\n}\nctx.ui.setTheme(lightTheme!);  // Or switch by Theme object\nctx.ui.theme.fg(\"accent\", \"styled text\");  // Access current theme\n```\n\n사용자 정의 작업 표시기 프레임은 그대로 렌더링됩니다. 색상을 원하면 `ctx.ui.theme.fg(...)` 등을 사용하여 프레임 문자열에 직접 추가하세요.\n\n### 자동완성 Providers\n\n`ctx.ui.addAutocompleteProvider()`를 사용하여 내장된 슬래시 명령 및 경로 제공자 위에 사용자 정의 자동 완성 논리를 쌓습니다. ``ctx.ui.addAutocompleteProvider()`를 사용하여 내장된 슬래시 명령 및 경로 제공자 위에 사용자 정의 자동 완성 논리를 쌓습니다. 와 같은 사용자 지정 자연 트리거의 경우 `triggerCharacters`를 설정합니다.\n\n일반적인 패턴:\n\n- 커서 앞의 텍스트를 검사합니다.\n- 확장 관련 구문이 일치하면 자체 제안을 반환합니다.\n- 그렇지 않으면 `current.getSuggestions(...)`에 위임하세요\n- 사용자 정의 삽입 동작이 필요하지 않은 경우 위임 `applyCompletion(...)`\n\n```typescript\npi.on(\"session_start\", (_event, ctx) => {\n  ctx.ui.addAutocompleteProvider((current) => ({\n    triggerCharacters: [\"#\"],\n    async getSuggestions(lines, cursorLine, cursorCol, options) {\n      const line = lines[cursorLine] ?? \"\";\n      const beforeCursor = line.slice(0, cursorCol);\n      const match = beforeCursor.match(/(?:^|[ \\t])#([^\\s#]*)$/);\n      if (!match) {\n        return current.getSuggestions(lines, cursorLine, cursorCol, options);\n      }\n\n      return {\n        prefix: `#${match[1] ?? \"\"}`,\n        items: [\n          { value: \"#2983\", label: \"#2983\", description: \"Extension API for registering custom @ autocomplete providers\" },\n          { value: \"#2753\", label: \"#2753\", description: \"Reload stale resource settings\" },\n        ],\n      };\n    },\n\n    applyCompletion(lines, cursorLine, cursorCol, item, prefix) {\n      return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);\n    },\n\n    shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {\n      return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;\n    },\n  }));\n});\n```\n\n`gh issue list`로 최신 공개 GitHub 이슈를 미리 로드하고 빠른 `#...` 완료를 위해 로컬로 필터링하는 전체 예제는 [github-issue-autocomplete.ts](../examples/extensions/github-issue-autocomplete.ts)를 참조하세요. GitHub CLI (`gh`) 및 GitHub 저장소 체크아웃이 필요합니다.\n\n### 맞춤 구성요소\n\n복잡한 UI의 경우 `ctx.ui.custom()`를 사용하세요. `done()`가 호출될 때까지 일시적으로 편집기가 구성 요소로 대체됩니다.\n\n```typescript\nimport { Text, Component } from \"@earendil-works/pi-tui\";\n\nconst result = await ctx.ui.custom<boolean>((tui, theme, keybindings, done) => {\n  const text = new Text(\"Press Enter to confirm, Escape to cancel\", 1, 1);\n\n  text.onKey = (key) => {\n    if (key === \"return\") done(true);\n    if (key === \"escape\") done(false);\n    return true;\n  };\n\n  return text;\n});\n\nif (result) {\n  // User pressed Enter\n}\n```\n\n콜백은 다음을 수신합니다.\n- `tui` - TUI 인스턴스 (화면 크기, 포커스 관리용)\n- `theme` - 스타일링을 위한 현재 테마\n- `keybindings` - 앱 키바인딩 관리자(단축키 확인용)\n- `done(value)` - 구성 요소를 닫고 값을 반환하는 호출\n\n전체 구성요소 API를 보려면 [tui.md](tui.md)를 참조하세요.\n\n#### 오버레이 모드(실험적)\n\n화면을 지우지 않고 기존 콘텐츠 위에 부동 모달로 구성 요소를 렌더링하려면 `{ overlay: true }`를 전달합니다.\n\n```typescript\nconst result = await ctx.ui.custom<string | null>(\n  (tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),\n  { overlay: true }\n);\n```\n\n고급 위치 지정(앵커, 여백, 백분율, 반응형 가시성)의 경우 `overlayOptions`를 전달하세요. `onHandle`를 사용하여 프로그래밍 방식으로 초점이나 가시성을 제어합니다.\n\n```typescript\nconst result = await ctx.ui.custom<string | null>(\n  (tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),\n  {\n    overlay: true,\n    overlayOptions: { anchor: \"top-right\", width: \"50%\", margin: 2 },\n    onHandle: (handle) => {\n      handle.focus(); // focus this overlay and bring it to the visual front\n      // handle.unfocus({ target: editorComponent }); // release input to a specific component\n      // handle.setHidden(true/false); // toggle visibility\n      // handle.hide(); // permanently remove\n    }\n  }\n);\n```\n\n집중된 표시 오버레이는 오버레이가 아닌 임시 사용자 정의 UI가 닫힌 후 입력을 회수할 수 있습니다. 오버레이가 계속 표시되는 동안 의도적으로 다른 구성요소가 입력을 유지하도록 하려면 `handle.unfocus({ target })`를 호출하세요. `{ target: null }`를 전달하면 다른 구성요소에 초점을 맞추지 않고 오버레이가 해제됩니다.\n\n전체 `OverlayOptions`는 [tui.md](tui.md)를, 예시는 `OverlayHandle` API 및 [overlay-qa-tests.ts](../examples/extensions/overlay-qa-tests.ts)를 참조하세요.\n\n### 맞춤 편집기\n\n기본 입력 편집기를 사용자 정의 구현(vim 모드, emacs 모드 등)으로 바꿉니다.\n\n```typescript\nimport { CustomEditor, type ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { matchesKey } from \"@earendil-works/pi-tui\";\n\nclass VimEditor extends CustomEditor {\n  private mode: \"normal\" | \"insert\" = \"insert\";\n\n  handleInput(data: string): void {\n    if (matchesKey(data, \"escape\") && this.mode === \"insert\") {\n      this.mode = \"normal\";\n      return;\n    }\n    if (this.mode === \"normal\" && data === \"i\") {\n      this.mode = \"insert\";\n      return;\n    }\n    super.handleInput(data);  // App keybindings + text editing\n  }\n}\n\nexport default function (pi: ExtensionAPI) {\n  pi.on(\"session_start\", (_event, ctx) => {\n    ctx.ui.setEditorComponent((tui, theme, keybindings) =>\n      new VimEditor(tui, theme, keybindings)\n    );\n  });\n}\n```\n\n**핵심 사항:**\n- `CustomEditor`(기본 `Editor` 아님)을 확장하여 앱 키 바인딩을 얻습니다(중단하려면 이스케이프, Ctrl+D, 모델 전환).\n- 처리할 수 없는 키는 `super.handleInput(data)`로 전화하세요.\n- 공장은 앱에서 `tui`, `theme`, `keybindings`를 받습니다.\n- 이전에 구성된 사용자 정의 편집기를 래핑하려면 `setEditorComponent()` 앞에 `ctx.ui.getEditorComponent()`를 사용하세요.\n- 기본값을 복원하려면 `undefined`를 전달하세요. `ctx.ui.setEditorComponent(undefined)`\n\n이미 편집기를 대체한 다른 확장으로 작성하려면 설정하기 전에 이전 팩토리를 캡처하세요.\n\n```typescript\nconst previous = ctx.ui.getEditorComponent();\nctx.ui.setEditorComponent((tui, theme, keybindings) =>\n  new MyEditor(tui, theme, keybindings, { base: previous?.(tui, theme, keybindings) })\n);\n```\n\n모드 표시기가 포함된 전체 예는 [tui.md](tui.md) 패턴 7을 참조하세요.\n\n### 메시지 및 항목 렌더링\n\n`customType`를 사용하여 메시지용 사용자 정의 렌더러를 등록하세요. LLM 컨텍스트에 참여해야 하는 콘텐츠에 대해 메시지 렌더러를 사용합니다.\n\n```typescript\nimport { Text } from \"@earendil-works/pi-tui\";\n\npi.registerMessageRenderer(\"my-extension\", (message, options, theme) => {\n  const { expanded, outputPad } = options;\n  let text = theme.fg(\"accent\", `[${message.customType}] `);\n  text += message.content;\n\n  if (expanded && message.details) {\n    text += \"\\n\" + theme.fg(\"dim\", JSON.stringify(message.details, null, 2));\n  }\n\n  return new Text(text, outputPad, 0);\n});\n```\n\n메시지는 `pi.sendMessage()`를 통해 전송됩니다.\n\n```typescript\npi.sendMessage({\n  customType: \"my-extension\",  // Matches registerMessageRenderer\n  content: \"Status update\",\n  display: true,               // Show in TUI\n  details: { ... },            // Available in renderer\n});\n```\n\nLLM으로 보내면 안 되는 TUI 전용 콘텐츠의 경우 대신 맞춤 항목을 렌더링하세요.\n\n```typescript\npi.registerEntryRenderer(\"my-card\", (entry, options, theme) => {\n  return new Text(theme.fg(\"accent\", JSON.stringify(entry.data)));\n});\n\npi.appendEntry(\"my-card\", { status: \"done\" });\n```\n\n### 테마 색상\n\n모든 렌더링 함수는 `theme` 객체를 받습니다. 사용자 정의 테마 및 전체 색상 팔레트를 생성하려면 [themes.md](themes.md)를 참조하세요.\n\n```typescript\n// Foreground colors\ntheme.fg(\"toolTitle\", text)   // Tool names\ntheme.fg(\"accent\", text)      // Highlights\ntheme.fg(\"success\", text)     // Success (green)\ntheme.fg(\"error\", text)       // Errors (red)\ntheme.fg(\"warning\", text)     // Warnings (yellow)\ntheme.fg(\"muted\", text)       // Secondary text\ntheme.fg(\"dim\", text)         // Tertiary text\n\n// Text styles\ntheme.bold(text)\ntheme.italic(text)\ntheme.strikethrough(text)\n```\n\n사용자 정의 도구 렌더러의 구문 강조 표시:\n\n```typescript\nimport { highlightCode, getLanguageFromPath } from \"@earendil-works/pi-coding-agent\";\n\n// Highlight code with explicit language\nconst highlighted = highlightCode(\"const x = 1;\", \"typescript\", theme);\n\n// Auto-detect language from file path\nconst lang = getLanguageFromPath(\"/path/to/file.rs\");  // \"rust\"\nconst highlighted = highlightCode(code, lang, theme);\n```\n\n## 오류 처리\n\n- 확장 프로그램 오류가 기록되고 에이전트가 계속됩니다.\n- `tool_call` 오류로 인해 도구가 차단됩니다(안전 장치).\n- 도구 `execute` 오류는 던짐으로 표시되어야 합니다. 발생한 오류가 포착되어 `isError: true`로 LLM에 보고되고 실행이 계속됩니다.\n\n## 모드 동작\n\n| 방법 | `ctx.mode` | `ctx.hasUI` | 메모 |\n|------|------------|-------------|-------|\n| 인터랙티브 | `\"tui\"` | `true` | 터미널 렌더링으로 전체 TUI |\n| RPC (`--mode rpc`) | `\"rpc\"` | `true` | JSON 프로토콜을 통한 대화상자 및 알림 `custom()`는 `undefined`를 반환합니다. [rpc.md](rpc.md) 참조 |\n| JSON (`--mode json`) | `\"json\"` | `false` | 이벤트 스트림을 stdout로; UI 메서드는 작동하지 않습니다. |\n| 인쇄(`-p`) | `\"print\"` | `false` | Extensions 실행했지만 메시지를 표시할 수 없음 |\n\nTUI 특정 기능(`custom()`, 컴포넌트 팩토리, 터미널 입력) 이전에는 `ctx.mode === \"tui\"`을 사용하세요. TUI 및 RPC 모드 모두에서 작동하는 대화 상자 및 알림 방법 전에 `ctx.hasUI`를 사용하세요.\n\n## 예시 참조\n\n[examples/extensions/](../examples/extensions/)의 모든 예시.\n\n| 예 | 설명 | 키 API |\n|---------|-------------|----------|\n| **도구** |  |  |\n| `hello.ts` | 최소한의 도구 등록 | `registerTool` |\n| `question.ts` | 사용자 상호작용이 가능한 도구 | `registerTool`, `ui.select` |\n| `questionnaire.ts` | 다단계 마법사 도구 | `registerTool`, `ui.custom` |\n| `todo.ts` | 지속성을 갖춘 상태 저장 도구 | `registerTool`, `appendEntry`, `renderResult`, 세션 이벤트 |\n| `dynamic-tools.ts` | 시작 후 및 명령 중에 도구 등록 | `registerTool`, `session_start`, `registerCommand` |\n| `structured-output.ts` | `terminate: true`를 사용한 최종 구조화된 출력 도구 | `registerTool`, 도구 결과 종료 |\n| `truncated-tool.ts` | 출력 잘림 예 | `registerTool`, `truncateHead` |\n| `tool-override.ts` | 내장된 읽기 도구 재정의 | `registerTool` (내장된 이름과 동일) |\n| **명령** |  |  |\n| `pirate.ts` | 턴당 시스템 프롬프트 수정 | `registerCommand`, `before_agent_start` |\n| `summarize.ts` | 대화 요약 명령 | `registerCommand`, `ui.custom` |\n| `handoff.ts` | 공급자 간 모델 핸드오프 | `registerCommand`, `ui.editor`, `ui.custom` |\n| `qna.ts` | 맞춤형 UI에 대한 Q&A | `registerCommand`, `ui.custom`, `setEditorText` |\n| `send-user-message.ts` | 사용자 메시지 삽입 | `registerCommand`, `sendUserMessage` |\n| `reload-runtime.ts` | 다시 로드 명령 및 LLM 도구 전달 | `registerCommand`, `ctx.reload()`, `sendUserMessage` |\n| `shutdown-command.ts` | 정상적인 종료 명령 | `registerCommand`, `shutdown()` |\n| **이벤트 및 게이트** |  |  |\n| `permission-gate.ts` | 위험한 명령 차단 | `on(\"tool_call\")`, `ui.confirm` |\n| `project-trust.ts` | 사용자/전역 또는 CLI 확장자로부터 프로젝트 신뢰를 결정하거나 연기합니다. | `on(\"project_trust\")`, 신뢰 UI, 필수 신뢰 결과 |\n| `protected-paths.ts` | 특정 경로에 대한 쓰기 차단 | `on(\"tool_call\")` |\n| `confirm-destructive.ts` | 세션 변경 사항 확인 | `on(\"session_before_switch\")`, `on(\"session_before_fork\")` |\n| `dirty-repo-guard.ts` | 더러운 git repo에 대해 경고 | `on(\"session_before_*\")`, `exec` |\n| `input-transform.ts` | 사용자 입력 변환 | `on(\"input\")` |\n| `input-transform-streaming.ts` | 스트리밍 인식 입력 변환 | `on(\"input\")`, `streamingBehavior` |\n| `model-status.ts` | React 모델 변경 | `on(\"model_select\")`, `setStatus` |\n| `provider-payload.ts` | 페이로드 및 공급자 응답 헤더 검사 | `on(\"before_provider_request\")`, `on(\"after_provider_response\")` |\n| `system-prompt-header.ts` | 시스템 프롬프트 정보 표시 | `on(\"agent_start\")`, `getSystemPrompt` |\n| `claude-rules.ts` | 파일에서 규칙 로드 | `on(\"session_start\")`, `on(\"before_agent_start\")` |\n| `prompt-customizer.ts` | `systemPromptOptions`를 사용하여 상황 인식 도구 안내 추가 | `on(\"before_agent_start\")`, `BuildSystemPromptOptions` |\n| `file-trigger.ts` | 파일 감시자가 메시지를 트리거합니다. | `sendMessage` |\n| **압축 및 세션** |  |  |\n| `custom-compaction.ts` | 사용자 정의 압축 요약 | `on(\"session_before_compact\")` |\n| `trigger-compact.ts` | 압축을 수동으로 트리거 | `compact()` |\n| `git-checkpoint.ts` | Git 턴에 보관함 | `on(\"turn_start\")`, `on(\"session_before_fork\")`, `exec` |\n| `git-merge-and-resolve.ts` | 충돌 가져오기, 병합 및 해결 | `on(\"agent_end\")`, `exec`, `sendUserMessage` |\n| `auto-commit-on-exit.ts` | 종료 시 커밋 | `on(\"session_shutdown\")`, `exec` |\n| **UI 구성요소** |  |  |\n| `status-line.ts` | 바닥글 상태 표시기 | `setStatus`, 세션 이벤트 |\n| `working-indicator.ts` | 스트리밍 작업 표시기 사용자 정의 | `setWorkingIndicator`, `registerCommand` |\n| `github-issue-autocomplete.ts` | `gh issue list`에서 최근 열린 이슈를 미리 로드하여 내장된 자동 완성 위에 `#1234` 이슈 완료를 추가하세요. | `addAutocompleteProvider`, `on(\"session_start\")`, `exec` |\n| `custom-footer.ts` | 바닥글 전체 바꾸기 | `registerCommand`, `setFooter` |\n| `custom-header.ts` | 시작 헤더 교체 | `on(\"session_start\")`, `setHeader` |\n| `modal-editor.ts` | Vim 스타일 모달 편집기 | `setEditorComponent`, `CustomEditor` |\n| `rainbow-editor.ts` | 사용자 정의 편집기 스타일링 | `setEditorComponent` |\n| `widget-placement.ts` | 편집기 위/아래 위젯 | `setWidget` |\n| `overlay-test.ts` | 오버레이 구성요소 | `ui.custom` 오버레이 옵션 포함 |\n| `overlay-qa-tests.ts` | 포괄적인 오버레이 테스트 | `ui.custom`, 모든 오버레이 옵션 |\n| `notify.ts` | 간단한 알림 | `ui.notify` |\n| `timed-confirm.ts` | 시간 초과가 있는 대화 상자 | `ui.confirm` 시간 초과/신호 포함 |\n| `mac-system-theme.ts` | 테마 자동 전환 | `setTheme`, `exec` |\n| **복잡함 Extensions** |  |  |\n| `plan-mode/` | 전체 계획 모드 구현 | 모든 이벤트 유형, `registerCommand`, `registerShortcut`, `registerFlag`, `setStatus`, `setWidget`, `sendMessage`, `setActiveTools` |\n| `preset.ts` | 저장 가능한 사전 설정(모델, 도구, 사고) | `registerCommand`, `registerShortcut`, `registerFlag`, `setModel`, `setActiveTools`, `setThinkingLevel`, `appendEntry` |\n| `tools.ts` | 도구 UI 켜기/끄기 전환 | `registerCommand`, `setActiveTools`, `SettingsList`, 세션 이벤트 |\n| **원격 및 샌드박스** |  |  |\n| `ssh.ts` | SSH 원격 실행 | `registerFlag`, `on(\"user_bash\")`, `on(\"before_agent_start\")`, 도구 작업 |\n| `interactive-shell.ts` | 영구 셸 세션 | `on(\"user_bash\")` |\n| `sandbox/` | 샌드박스 도구 실행 | 도구 작업 |\n| `gondolin/` | 내장 도구와 `!` 명령을 Gondolin 마이크로 VM으로 라우팅 | 도구 작업, 내장 도구 재정의, `on(\"user_bash\")` |\n| `subagent/` | 하위 에이전트 생성 | `registerTool`, `exec` |\n| **계략** |  |  |\n| `snake.ts` | 뱀 게임 | `registerCommand`, `ui.custom`, 키보드 처리 |\n| `space-invaders.ts` | 스페이스 인베이더 게임 | `registerCommand`, `ui.custom` |\n| `doom-overlay/` | 오버레이의 파멸 | `ui.custom` 오버레이 포함 |\n| **Providers** |  |  |\n| `custom-provider-anthropic/` | 맞춤형 인류 프록시 | `registerProvider` |\n| `custom-provider-gitlab-duo/` | GitLab Duo 통합 | `registerProvider`와 OAuth |\n| **메시지 및 커뮤니케이션** |  |  |\n| `message-renderer.ts` | 사용자 정의 메시지 렌더링 | `registerMessageRenderer`, `sendMessage` |\n| `entry-renderer.ts` | TUI 전용 사용자 정의 항목 렌더링 | `registerEntryRenderer`, `appendEntry` |\n| `event-bus.ts` | 확장 간 이벤트 | `pi.events` |\n| **세션 메타데이터** |  |  |\n| `session-name.ts` | 선택자의 세션 이름 지정 | `setSessionName`, `getSessionName` |\n| `bookmark.ts` | /tree에 대한 북마크 항목 | `setLabel` |\n| **기타** |  |  |\n| `inline-bash.ts` | 도구 호출의 인라인 bash | `on(\"tool_call\")` |\n| `bash-spawn-hook.ts` | 실행 전 bash 명령어, cwd, env 조정 | `createBashTool`, `spawnHook` |\n| `with-deps/` | npm 종속성이 있는 확장 | `package.json`를 사용한 패키지 구조 |","sourceFile":"extensions.md"},"index":{"title":"Pi 문서","markdown":"Pi는 최소한의 터미널 코딩 하니스입니다. TypeScript 확장, 기술, prompt templates, 테마 및 pi 패키지를 통해 확장되면서 핵심을 작게 유지하도록 설계되었습니다.\n\n## 빠른 시작\n\nnpm로 Pi를 설치하세요:\n\n```bash\nnpm install -g --ignore-scripts @earendil-works/pi-coding-agent\n```\n\n`--ignore-scripts` 설치 중에 종속성 수명 주기 스크립트를 비활성화합니다. Pi 일반 npm 설치에는 설치 스크립트가 필요하지 않습니다.\n\nLinux 또는 macOS에서는 설치 프로그램을 사용할 수도 있습니다.\n\n```bash\ncurl -fsSL https://pi.dev/install.sh | sh\n```\n\npi 자체를 제거하려면 컬에 npm를 사용하고 설치에 npm를 사용하세요.\n\n```bash\nnpm uninstall -g @earendil-works/pi-coding-agent\n```\n\npnpm, Yarn 또는 Bun 설치의 경우 일치하는 전역 제거 명령(`pnpm remove -g @earendil-works/pi-coding-agent`, `yarn global remove @earendil-works/pi-coding-agent` 또는 `bun uninstall -g @earendil-works/pi-coding-agent`)을 사용합니다.\n\n그런 다음 프로젝트 디렉터리에서 실행합니다.\n\n```bash\npi\n```\n\nsubscription providers에 대해 `/login`로 인증하거나 pi를 시작하기 전에 `ANTHROPIC_API_KEY`와 같은 API key를 설정하세요.\n\n전체 첫 실행 흐름은 [Quickstart](quickstart.md)를 참조하세요.\n\n## 여기서 시작하세요\n\n- [Quickstart](quickstart.md) - 첫 번째 세션을 설치, 인증 및 실행합니다.\n- [Using Pi](usage.md) - 대화형 모드, slash commands, context files 및 CLI 참조.\n- [Providers](providers.md) - 내장 공급자를 위한 구독 및 API 키 설정.\n- [llama.cpp](llama-cpp.md) - `/llama`로 로컬 라우터를 실행하고 모델을 관리합니다.\n- [Security](security.md) - 프로젝트 신뢰, sandbox 경계 및 취약점 보고.\n- [Containerization](containerization.md) - sandbox 파이는 Gondolin, Docker 또는 OpenShell로 표시됩니다.\n- [Settings](settings.md) - 전역 및 프로젝트 설정.\n- [Keybindings](keybindings.md) - 기본 단축키 및 맞춤 키 바인딩.\n- [Sessions](sessions.md) - 세션 관리, 분기 및 트리 탐색.\n- [Compaction](compaction.md) - context compaction 및 branch summarization.\n\n## 맞춤화\n\n- [Extensions](extensions.md) - TypeScript 도구, 명령, 이벤트 및 사용자 정의 UI를 위한 모듈입니다.\n- [Skills](skills.md) - 재사용 가능한 주문형 기능을 위한 에이전트 Skills.\n- [Prompt templates](prompt-templates.md) - slash commands에서 확장되는 재사용 가능한 프롬프트입니다.\n- [Themes](themes.md) - 내장 및 사용자 정의 terminal themes.\n- [Pi packages](packages.md) - 확장 프로그램, 기술, 프롬프트 및 테마를 묶고 공유합니다.\n- [Custom models](models.md) - 지원되는 공급자 API에 대한 모델 항목을 추가합니다.\n- [Custom providers](custom-provider.md) - 맞춤 API 및 OAuth 흐름을 구현합니다.\n\n## 프로그래밍 방식의 사용\n\n- [SDK](sdk.md) - Node.js 애플리케이션에 파이를 삽입합니다.\n- [RPC mode](rpc.md) - stdin/stdout JSONL 이상을 통합합니다.\n- [JSON event stream mode](json.md) - 구조화된 이벤트가 포함된 인쇄 모드입니다.\n- [TUI components](tui.md) - 확장 기능을 위한 사용자 정의 터미널 UI를 구축합니다.\n\n## 참조\n\n- [Environment variables](environment-variables.md) - Pi 프로세스 구성 및 세션 메타데이터를 bash 도구에 사용할 수 있습니다.\n- [Session format](session-format.md) - JSONL 세션 파일 형식, 항목 유형 및 SessionManager API.\n\n## 플랫폼 설정\n\n- [Windows](windows.md)\n- [Termux on Android](termux.md)\n- [tmux](tmux.md)\n- [Terminal setup](terminal-setup.md)\n- [Shell aliases](shell-aliases.md)\n\n## 개발\n\n- [Development](development.md) - 로컬 설정, 프로젝트 구조 및 디버깅.","sourceFile":"index.md"},"json":{"title":"JSON 이벤트 스트림 모드","markdown":"```bash\npi --mode json \"Your prompt\"\n```\n\n모든 세션 이벤트를 JSON 라인에서 stdout 라인으로 출력합니다. pi를 다른 도구나 사용자 정의 UI에 통합하는 데 유용합니다.\n\n## 이벤트 유형\n\n와이어 이벤트는 `JsonAgentSessionEvent`를 사용합니다. 일치합니다\n[`AgentSessionEvent`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/agent-session.ts)\n단, 스트리밍 메시지 업데이트에서는 누적 스냅샷이 생략됩니다.\n\n```typescript\ntype WithoutPartial<T> = T extends { partial: unknown } ? Omit<T, \"partial\"> : T;\n\ntype JsonAgentSessionEvent =\n  | Exclude<AgentSessionEvent, { type: \"message_update\" }>\n  | {\n      type: \"message_update\";\n      assistantMessageEvent: WithoutPartial<AssistantMessageEvent>;\n    };\n```\n\n`queue_update` 변경될 때마다 보류 중인 전체 조정 및 후속 조치 대기열을 내보냅니다. `compaction_start` 및 `compaction_end`는 수동 및 자동 압축을 모두 다룹니다.\n\n다른 기본 이벤트는 다음에서 제공됩니다.\n[`AgentEvent`](https://github.com/earendil-works/pi-mono/blob/main/packages/agent/src/types.ts):\n\n```typescript\ntype AgentEvent =\n  // Agent lifecycle\n  | { type: \"agent_start\" }\n  | { type: \"agent_end\"; messages: AgentMessage[] }\n  // Turn lifecycle\n  | { type: \"turn_start\" }\n  | { type: \"turn_end\"; message: AgentMessage; toolResults: ToolResultMessage[] }\n  // Message lifecycle\n  | { type: \"message_start\"; message: AgentMessage }\n  | { type: \"message_update\"; message: AgentMessage; assistantMessageEvent: AssistantMessageEvent }\n  | { type: \"message_end\"; message: AgentMessage }\n  // Tool execution\n  | { type: \"tool_execution_start\"; toolCallId: string; toolName: string; args: any }\n  | { type: \"tool_execution_update\"; toolCallId: string; toolName: string; args: any; partialResult: any }\n  | { type: \"tool_execution_end\"; toolCallId: string; toolName: string; result: any; isError: boolean };\n```\n\n## 메시지 유형\n\n[`packages/ai/src/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/types.ts#L134)의 기본 메시지:\n- `UserMessage` (134행)\n- `AssistantMessage` (140행)\n- `ToolResultMessage` (152행)\n\n[`packages/coding-agent/src/core/messages.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/messages.ts#L29)의 확장 메시지:\n- `BashExecutionMessage` (29행)\n- `CustomMessage` (46행)\n- `BranchSummaryMessage` (55번째 줄)\n- `CompactionSummaryMessage` (62행)\n\n## 출력 형식\n\n각 줄은 JSON 객체입니다. 첫 번째 줄은 세션 헤더입니다.\n\n```json\n{\"type\":\"session\",\"version\":3,\"id\":\"uuid\",\"timestamp\":\"...\",\"cwd\":\"/path\"}\n```\n\n다음과 같은 이벤트가 발생합니다.\n\n```json\n{\"type\":\"agent_start\"}\n{\"type\":\"turn_start\"}\n{\"type\":\"message_start\",\"message\":{\"role\":\"assistant\",\"content\":[],...}}\n{\"type\":\"message_update\",\"assistantMessageEvent\":{\"type\":\"text_delta\",\"contentIndex\":0,\"delta\":\"Hello\"}}\n{\"type\":\"message_end\",\"message\":{...}}\n{\"type\":\"turn_end\",\"message\":{...},\"toolResults\":[]}\n{\"type\":\"agent_end\",\"messages\":[...]}\n```\n\n`message_update` 레코드는 델타 전용입니다. 누적 `message` 필드와\n`assistantMessageEvent.partial` 스트림 크기를 선형으로 유지합니다. `contentIndex` 및 `delta` 사용\n필요한 경우 실시간 텍스트, 사고 또는 도구 호출 인수를 조합합니다. `message_end` 포함\n최종 권위 있는 메시지.\n\n## 예\n\n```bash\npi --mode json \"List files\" 2>/dev/null | jq -c 'select(.type == \"message_end\")'\n```","sourceFile":"json.md"},"keybindings":{"title":"키바인딩","markdown":"모든 키보드 단축키는 `~/.pi/agent/keybindings.json`를 통해 맞춤설정할 수 있습니다. 각 작업은 하나 이상의 키에 바인딩될 수 있습니다.\n\n구성 파일은 pi가 내부적으로 사용하고 확장 작성자가 `keyHint()` 및 삽입된 `keybindings` 관리자에서 사용하는 것과 동일한 네임스페이스 키 바인딩 ID를 사용합니다.\n\n`cursorUp` 또는 `expandTools`와 같은 사전 네임스페이스 ID를 사용하는 이전 구성은 시작 시 네임스페이스 ID로 자동으로 마이그레이션됩니다.\n\n`keybindings.json`를 편집한 후 pi에서 `/reload`를 실행하면 세션을 다시 시작하지 않고도 변경 사항이 적용됩니다.\n\n## 키 형식\n\n`modifier+key` 수정자는 `ctrl`, `shift`, `alt`, `super`(결합 가능)이고 키는 다음과 같습니다.\n\n- **문자:** `a-z`\n- **숫자:** `0-9`\n- **특수 키:** `escape`, `esc`, `enter`, `return`, `tab`, `space`, `backspace`, `delete`, `insert`, `clear`, `home`, `end`, `pageUp`, `pageDown`, `up`, `down`, `left`, `right`\n- **기능 키:** `f1`-`f12`\n- **기호:** `` ` ``, `-`, `=`, `[`, `]`, `\\`, `;`, `'`, `,`, `.`, `/`, `!`, `@`, `#`, `$`, `%`, `^`, `&`, `*`, `(`, `)`, `_`, `+`, `|`, `~`, `{`, `}`, `:`, `<`, `>`, `?`\n\n수정자 조합: `ctrl+shift+x`, `alt+ctrl+x`, `ctrl+shift+alt+x`, `super+k`, `ctrl+super+k`, `ctrl+1` 등\n\n`super` 바인딩에는 일반적으로 Kitty 키보드 프로토콜을 통해 수정자를 별도로 보고하는 터미널이 필요합니다. 해당 지원이 없으면 터미널에서 작동하지 않을 수 있습니다.\n\n## 모든 작업\n\n### TUI 에디터 커서 이동\n\n| 키바인딩 ID | 기본 | 설명 |\n|--------|---------|-------------|\n| `tui.editor.cursorUp` | `up` | 커서를 위로 이동하고 상단에서 이전 기록을 찾아보세요. |\n| `tui.editor.cursorDown` | `down` | 커서를 아래로 이동하여 하단에서 최신 기록을 찾아보세요. |\n| `tui.editor.historyPrevious` | *(없음)* | 이전 프롬프트 기록 항목을 선택하세요. |\n| `tui.editor.historyNext` | *(없음)* | 다음 프롬프트 기록 항목을 선택하세요. |\n| `tui.editor.cursorLeft` | `left`, `ctrl+b` | 커서를 왼쪽으로 이동 |\n| `tui.editor.cursorRight` | `right`, `ctrl+f` | 커서를 오른쪽으로 이동 |\n| `tui.editor.cursorWordLeft` | `alt+left`, `ctrl+left`, `alt+b` | 커서 단어를 왼쪽으로 이동 |\n| `tui.editor.cursorWordRight` | `alt+right`, `ctrl+right`, `alt+f` | 커서 단어를 오른쪽으로 이동 |\n| `tui.editor.cursorLineStart` | `home`, `ctrl+home`, `ctrl+a` | 줄 시작으로 이동 |\n| `tui.editor.cursorLineEnd` | `end`, `ctrl+end`, `ctrl+e` | 줄 끝으로 이동 |\n| `tui.editor.jumpForward` | `ctrl+]` | 캐릭터 앞으로 이동 |\n| `tui.editor.jumpBackward` | `ctrl+alt+]` | 캐릭터 뒤로 이동 |\n| `tui.editor.pageUp` | `pageUp`, `ctrl+pageUp` | 페이지별로 위로 스크롤 |\n| `tui.editor.pageDown` | `pageDown`, `ctrl+pageDown` | 페이지별로 아래로 스크롤 |\n\n전용 내역 작업은 여러 줄 프롬프트의 커서 위치에 관계없이 항상 내역 항목을 변경합니다. 명시적 기록 바인딩은 기본 편집기에 초점이 맞춰져 있는 동안 애플리케이션 작업보다 우선하므로 `tui.editor.historyPrevious`에서 `ctrl+p`로 바인딩하면 선택기에서 `Ctrl+P`를 변경하지 않고 해당 컨텍스트에서 모델 순환을 재정의합니다.\n\n### TUI 편집자 삭제\n\n| 키바인딩 ID | 기본 | 설명 |\n|--------|---------|-------------|\n| `tui.editor.deleteCharBackward` | `backspace` | 문자를 뒤로 삭제 |\n| `tui.editor.deleteCharForward` | `delete`, `ctrl+d` | 앞으로 문자 삭제 |\n| `tui.editor.deleteWordBackward` | `ctrl+w`, `alt+backspace` | 뒤로 단어 삭제 |\n| `tui.editor.deleteWordForward` | `alt+d`, `alt+delete` | 앞으로 단어 삭제 |\n| `tui.editor.deleteToLineStart` | `ctrl+u` | 줄 시작까지 삭제 |\n| `tui.editor.deleteToLineEnd` | `ctrl+k` | 줄 끝까지 삭제 |\n\n### TUI 입력\n\n| 키바인딩 ID | 기본 | 설명 |\n|--------|---------|-------------|\n| `tui.input.newLine` | `shift+enter`, `ctrl+j` | 새 줄 삽입 |\n| `tui.input.submit` | `enter` | 입력 제출 |\n| `tui.input.tab` | `tab` | 탭/자동완성 |\n\n### TUI 킬링\n\n| 키바인딩 ID | 기본 | 설명 |\n|--------|---------|-------------|\n| `tui.editor.yank` | `ctrl+y` | 가장 최근에 삭제한 텍스트 붙여넣기 |\n| `tui.editor.yankPop` | `alt+y` | 잡아당긴 후 삭제된 텍스트를 순환합니다. |\n| `tui.editor.undo` | `ctrl+-` | 마지막 편집 취소 |\n\n### TUI 클립보드 및 선택\n\n| 키바인딩 ID | 기본 | 설명 |\n|--------|---------|-------------|\n| `tui.input.copy` | `ctrl+c` | 선택 항목 복사 |\n| `tui.select.up` | `up` | 선택 항목을 위로 이동 |\n| `tui.select.down` | `down` | 선택 항목을 아래로 이동 |\n| `tui.select.pageUp` | `pageUp` | 목록에서 한 페이지 위로 |\n| `tui.select.pageDown` | `pageDown` | 목록에서 페이지 아래로 |\n| `tui.select.confirm` | `enter` | 선택 확인 |\n| `tui.select.cancel` | `escape`, `ctrl+c` | 선택 취소 |\n\n### TUI 전체 화면 뷰포트\n\n이러한 작업은 대화형 모드가 `--tui-mode fullscreen`를 사용하고 기본 성적표 스크롤 영역을 대상으로 할 때 적용됩니다. 두 손가락 트랙패드 및 마우스 휠 입력은 포인터 아래 영역을 스크롤하여 고정 편집기/상태/바닥글 도크 위의 내용으로 돌아갑니다. OSC 8 하이퍼링크를 클릭하면 기본 처리기에서 열립니다. 기본 마우스 버튼으로 드래그하면 텍스트가 선택되어 클립보드에 복사됩니다. 성적 내용의 상단 또는 하단 가장자리를 길게 누르면 화면 밖의 콘텐츠로 자동 스크롤됩니다.\n\n전체 화면 기록 바인딩은 편집기 바인딩보다 우선합니다. 따라서 수정되지 않은 기본 탐색 키는 전체 화면 모드에서 스크립트를 제어하는 ​​반면, `ctrl` 변형 키는 계속해서 편집기를 제어합니다. 전체 화면 모드 외부에서는 두 변형 모두 편집기를 제어합니다.\n\n| 열쇠 | 기본 모드 | 전체 화면 모드 |\n|-----|--------------|-----------------|\n| `home`, `end` | 편집자 | 성적 증명서 |\n| `ctrl+home`, `ctrl+end` | 편집자 | 편집자 |\n| `pageUp`, `pageDown` | 편집자 | 성적 증명서 |\n| `ctrl+pageUp`, `ctrl+pageDown` | 편집자 | 편집자 |\n\n이 라우팅은 일반 작업 바인딩을 통해 구성 가능한 상태로 유지됩니다. 예를 들어 `\"tui.altScreen.pageUp\": \"ctrl+pageUp\"`는 `pageUp`로 편집기를 제어하고 `ctrl+pageUp`는 전체 화면 모드에서 스크립트를 제어합니다. 전체 페이지 바인딩을 유지하면서 더 작은 스크립트 단계를 위해 `tui.altScreen.halfPageUp` 및 `tui.altScreen.halfPageDown`를 바인딩합니다. `\"tui.altScreen.pageUp\": []`를 설정하면 해당 스크립트 바로가기가 완전히 비활성화됩니다. 사용자 바인딩은 해당 작업의 기본값을 대체합니다.\n\n| 키바인딩 ID | 기본 | 설명 |\n|--------|---------|-------------|\n| `tui.altScreen.pageUp` | `pageUp` | 스크립트를 한 페이지 위로 스크롤 |\n| `tui.altScreen.pageDown` | `pageDown` | 스크립트를 한 페이지 아래로 스크롤하세요. |\n| `tui.altScreen.halfPageUp` | *(없음)* | 대본을 반 페이지 위로 스크롤 |\n| `tui.altScreen.halfPageDown` | *(없음)* | 스크립트를 반 페이지 아래로 스크롤하세요. |\n| `tui.altScreen.previousPrompt` | `ctrl+shift+up` | 표시된 이전 메시지로 이동 |\n| `tui.altScreen.nextPrompt` | `ctrl+shift+down` | 표시된 다음 메시지로 이동 |\n| `tui.altScreen.top` | `home` | 스크립트 시작 부분으로 스크롤 |\n| `tui.altScreen.bottom` | `end` | 스크립트 끝으로 스크롤하고 새 출력을 따릅니다. |\n\n### 애플리케이션\n\n| 키바인딩 ID | 기본 | 설명 |\n|--------|---------|-------------|\n| `app.interrupt` | `escape` | 취소/중단 |\n| `app.clear` | `ctrl+c` | 편집기 지우기(첫 번째) / 종료(두 번째) |\n| `app.exit` | `ctrl+d` | 종료(편집기가 비어 있는 경우) |\n| `app.suspend` | `ctrl+z` (Windows에서는 없음) | 배경으로 일시중지 |\n| `app.editor.external` | `ctrl+g` | 외부 편집기에서 열기(`externalEditor`, `$VISUAL`, `$EDITOR`, Windows의 메모장 또는 다른 곳에서는 `nano`) |\n| `app.clipboard.pasteImage` | `ctrl+v` (Windows의 경우 `alt+v`) | 클립보드의 이미지나 텍스트 붙여넣기 |\n\n### 세션\n\n| 키바인딩 ID | 기본 | 설명 |\n|--------|---------|-------------|\n| `app.session.new` | *(없음)* | 새 세션 시작(`/new`) |\n| `app.session.tree` | *(없음)* | session tree 탐색기 열기(`/tree`) |\n| `app.session.fork` | *(없음)* | 현재 세션 포크(`/fork`) |\n| `app.session.resume` | *(없음)* | 세션 이력서 선택기 열기(`/resume`) |\n| `app.session.togglePath` | `ctrl+p` | 경로 표시 전환 |\n| `app.session.toggleSort` | `ctrl+s` | 정렬 모드 전환 |\n| `app.session.toggleNamedFilter` | `ctrl+n` | 명명된 전용 필터 전환 |\n| `app.session.rename` | `ctrl+r` | 세션 이름 바꾸기 |\n| `app.session.delete` | `ctrl+d` | 세션 삭제 |\n| `app.session.deleteNoninvasive` | `ctrl+backspace` | 쿼리가 비어 있으면 세션 삭제 |\n\n### Models 그리고 생각하기\n\n| 키바인딩 ID | 기본 | 설명 |\n|--------|---------|-------------|\n| `app.model.select` | `ctrl+l` | 모델 선택기 열기 |\n| `app.model.cycleForward` | `ctrl+p` | 다음 모델로 순환 |\n| `app.model.cycleBackward` | `shift+ctrl+p` | 이전 모델로 순환 |\n| `app.thinking.cycle` | `shift+tab` | 주기적인 사고 수준 |\n| `app.thinking.toggle` | `ctrl+t` | 사고 블록 축소 또는 확장 |\n\n### 디스플레이 및 메시지 대기열\n\n| 키바인딩 ID | 기본 | 설명 |\n|--------|---------|-------------|\n| `app.tools.expand` | `ctrl+o` | 도구 출력 축소 또는 확장 |\n| `app.message.copy` | `ctrl+x` | 마지막 보조 메시지 또는 `/tree`에서 선택한 메시지를 복사하세요. |\n| `app.message.followUp` | `alt+enter` | 대기열 후속 메시지 |\n| `app.message.dequeue` | `alt+up` | 대기 중인 메시지를 편집기로 복원 |\n\n### 트리 탐색\n\n| 키바인딩 ID | 기본 | 설명 |\n|--------|---------|-------------|\n| `app.tree.foldOrUp` | `ctrl+left`, `alt+left` | 현재 분기 세그먼트를 접거나 이전 세그먼트 시작으로 점프 |\n| `app.tree.unfoldOrDown` | `ctrl+right`, `alt+right` | 현재 분기 세그먼트를 펼치거나 다음 세그먼트 시작 또는 분기 끝으로 점프합니다. |\n| `app.tree.editLabel` | `shift+l` | 선택한 트리 노드의 레이블을 편집합니다. |\n| `app.tree.toggleLabelTimestamp` | `shift+t` | 트리에서 라벨 타임스탬프 전환 |\n| `app.tree.filter.default` | `ctrl+d` | 트리 필터를 기본 보기로 설정 |\n| `app.tree.filter.noTools` | `ctrl+t` | 도구 결과를 숨기는 트리 필터 전환 |\n| `app.tree.filter.userOnly` | `ctrl+u` | 사용자 메시지만 표시하는 트리 필터 전환 |\n| `app.tree.filter.labeledOnly` | `ctrl+l` | 레이블이 지정된 항목만 표시하는 트리 필터 전환 |\n| `app.tree.filter.all` | `ctrl+a` | 모든 항목을 표시하는 트리 필터 전환 |\n| `app.tree.filter.cycleForward` | `ctrl+o` | 순환 트리 필터 전달 |\n| `app.tree.filter.cycleBackward` | `shift+ctrl+o` | 순환 트리 필터를 뒤로 |\n\n### 범위 지정 Models 선택기\n\n범위가 지정된 모델 선택기 내부에서 사용됩니다(`/scoped-models`를 통해 열림).\n\n| 키바인딩 ID | 기본 | 설명 |\n|--------|---------|-------------|\n| `app.models.save` | `ctrl+s` | 현재 모델 선택을 설정에 저장 |\n| `app.models.enableAll` | `ctrl+a` | 모든 모델 활성화(또는 현재 검색과 일치하는 모든 모델) |\n| `app.models.clearAll` | `ctrl+x` | 모든 모델 지우기(또는 현재 검색과 일치하는 모든 모델) |\n| `app.models.toggleProvider` | `ctrl+p` | 현재 공급자의 모든 모델을 전환합니다. |\n| `app.models.reorderUp` | `alt+up` | 선택한 모델을 주기 순서에서 위로 이동합니다. |\n| `app.models.reorderDown` | `alt+down` | 선택한 모델을 주기 순서에서 아래로 이동합니다. |\n\n## 맞춤 구성\n\n`~/.pi/agent/keybindings.json` 만들기:\n\n```json\n{\n  \"tui.editor.historyPrevious\": \"ctrl+p\",\n  \"tui.editor.historyNext\": \"ctrl+n\",\n  \"tui.editor.deleteWordBackward\": [\"ctrl+w\", \"alt+backspace\"]\n}\n```\n\n각 작업에는 단일 키 또는 키 배열이 있을 수 있습니다. 사용자 구성은 기본값을 재정의합니다.\n\n기본 Windows에서는 Windows 터미널이 Unix 작업 제어를 지원하지 않기 때문에 `app.suspend`에 기본 바인딩이 없습니다. 수동으로 바인딩하면 pi는 일시 중지 대신 상태 메시지를 표시합니다. WSL에서는 일반적인 Linux `ctrl+z`/`fg` 동작이 계속 적용됩니다.\n\n### 이맥스 예\n\n```json\n{\n  \"tui.editor.historyPrevious\": \"ctrl+p\",\n  \"tui.editor.historyNext\": \"ctrl+n\",\n  \"tui.editor.cursorLeft\": [\"left\", \"ctrl+b\"],\n  \"tui.editor.cursorRight\": [\"right\", \"ctrl+f\"],\n  \"tui.editor.cursorWordLeft\": [\"alt+left\", \"alt+b\"],\n  \"tui.editor.cursorWordRight\": [\"alt+right\", \"alt+f\"],\n  \"tui.editor.deleteCharForward\": [\"delete\", \"ctrl+d\"],\n  \"tui.editor.deleteCharBackward\": [\"backspace\", \"ctrl+h\"],\n  \"tui.input.newLine\": [\"shift+enter\", \"ctrl+j\"]\n}\n```\n\n### 빔 예\n\n```json\n{\n  \"tui.editor.cursorUp\": [\"up\", \"alt+k\"],\n  \"tui.editor.cursorDown\": [\"down\", \"alt+j\"],\n  \"tui.editor.cursorLeft\": [\"left\", \"alt+h\"],\n  \"tui.editor.cursorRight\": [\"right\", \"alt+l\"],\n  \"tui.editor.cursorWordLeft\": [\"alt+left\", \"alt+b\"],\n  \"tui.editor.cursorWordRight\": [\"alt+right\", \"alt+w\"]\n}\n```","sourceFile":"keybindings.md"},"llama-cpp":{"title":"llama.cpp","markdown":"Pi는 [llama.cpp](https://github.com/ggml-org/llama.cpp) 라우터 서버를 지원합니다. 라우터는 여러 GGUF 모델을 발견하고 요청 시 이를 로드하거나 언로드합니다.\n\n라우터를 지원하는 최신 llama.cpp 빌드를 사용하세요. [build instructions](https://github.com/ggml-org/llama.cpp/blob/master/docs/build.md)를 따르거나 플랫폼에 맞는 [prebuilt release](https://github.com/ggml-org/llama.cpp/releases)를 설치하세요.\n\n## 라우터 시작\n\n`--model` 또는 `-m` 없이 `llama-server`를 시작하세요. 모델을 전달하면 라우터 모드 대신 단일 모델 모드가 시작됩니다.\n\n```bash\nllama-server \\\n  --models-dir ~/models \\\n  --no-models-autoload \\\n  --jinja \\\n  --host 127.0.0.1 \\\n  --port 8080 \\\n  -ngl 999 \\\n  -c 32768\n```\n\n중요 옵션:\n\n- `--models-dir ~/models` 로컬 GGUF 파일을 검색합니다.\n- `--no-models-autoload`는 `/llama`을 통해 명시적인 항목을 계속 로드합니다.\n- `--jinja` 호환 가능한 채팅 템플릿과 도구 호출이 가능합니다.\n- `-ngl 999` 최대한 많은 레이어를 GPU로 오프로드합니다.\n- `-c 32768`는 로드된 각 모델에 대한 컨텍스트 창을 설정합니다. 모델의 기본 컨텍스트를 사용하려면 이를 생략하세요. 이 경우 훨씬 더 많은 메모리가 필요할 수 있습니다.\n\n단일 파일 모델은 모델 디렉터리에 직접 위치할 수 있습니다. 다중 모드 및 다중 샤드 모델을 별도의 하위 디렉터리에 넣습니다.\n\n```text\n~/models/\n├── llama-3.2-1b-Q4_K_M.gguf\n├── gemma-3-4b-it-Q4_K_M/\n│   ├── gemma-3-4b-it-Q4_K_M.gguf\n│   └── mmproj-F16.gguf\n└── large-model-Q4_K_M/\n    ├── large-model-Q4_K_M-00001-of-00003.gguf\n    ├── large-model-Q4_K_M-00002-of-00003.gguf\n    └── large-model-Q4_K_M-00003-of-00003.gguf\n```\n\n수동으로 파일을 추가한 후 라우터를 다시 시작하세요. 모델별 컨텍스트 크기 및 기타 옵션을 보려면 [llama.cpp model presets](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md#model-presets)를 사용하세요.\n\n## 구성 Pi\n\nPi를 시작하고 공급자를 구성합니다.\n\n```text\n/login llama.cpp\n```\n\n라우터 URL을 입력하고 선택사항 API key을 입력하세요. 기본 URL은 `http://127.0.0.1:8080`입니다.\n\n환경 변수는 `/login` 없이 동일한 값을 구성할 수 있습니다.\n\n```bash\nexport LLAMA_BASE_URL=http://127.0.0.1:8080\nexport LLAMA_API_KEY=optional-secret\npi\n```\n\n서버가 API key를 사용하는 경우 일치하는 `--api-key` 값으로 `llama-server`를 시작합니다. 로컬 전용 액세스의 경우 `--host 127.0.0.1`를 유지하세요.\n\n## 모델 관리\n\n달리다:\n\n```text\n/llama\n```\n\n- 언로드된 모델을 선택하여 로드하세요.\n- 로드된 모델을 선택하여 언로드합니다.\n- **모델 다운로드…**를 선택하고 Hugging Face를 검색한 다음 저장소와 양자화를 선택합니다. 정확한 `owner/repository[:quant]` 값도 작동합니다.\n- 취소를 확인하려면 로드 또는 다운로드 중에 Esc 키를 누르세요.\n\nHugging Face 검색은 설정 시 `HF_TOKEN`를 사용한 후 `$HF_TOKEN_PATH`, `$HF_HOME/token`, `$XDG_CACHE_HOME/huggingface/token`, `~/.cache/huggingface/token`를 확인합니다. 검색은 인증 없이도 작동하며 더 낮은 비율 제한이 적용됩니다. Pi 제한 저장소와 액세스 페이지 링크를 다운로드하기 전에 경고합니다. llama.cpp 서버는 다운로드를 수행하므로 선택한 저장소에 액세스가 필요할 때 해당 프로세스에도 `HF_TOKEN`가 있어야 합니다.\n\n다른 모델이 로드된 경우 Pi는 해당 모델을 먼저 언로드할지 아니면 계속 로드할지 묻습니다. Pi는 모델을 자동으로 언로드하지 않으며 모델 파일을 삭제하지 않습니다. 라우터는 다른 클라이언트와 공유될 수 있으므로 `/llama`는 항상 라우터의 현재 상태를 표시합니다.\n\n`/model`에는 로드된 모델만 나타납니다. 모델을 로드한 후 `/model`를 실행하여 현재 Pi 세션에 대해 모델을 선택합니다.\n\n라우터 연결이 끊어지면 `/llama`에 **재시도** 및 **닫기**가 표시됩니다. 재시도는 중단된 작업을 재생하지 않고 모델 상태를 다시 연결하고 새로 고칩니다.\n\n## 문제 해결\n\n라우터에 연결할 수 있는지 확인하세요.\n\n```bash\ncurl http://127.0.0.1:8080/health\ncurl http://127.0.0.1:8080/models\n```\n\n- **`/llama`에 모델이 없습니다:** `--models-dir` 디렉토리 레이아웃을 확인하고 라우터를 다시 시작하세요.\n- **`/model`에서 누락된 모델:** 먼저 `/llama`로 로드하세요.\n- **로드 실패 또는 너무 많은 메모리 사용:** `-c`를 낮추거나 다른 모델을 언로드합니다.\n- **서버가 라우터 모드에 있지 않습니다.** `--model`, `-m` 또는 `-hf` 없이 시작하세요.","sourceFile":"llama-cpp.md"},"models":{"title":"맞춤 Models","markdown":"`~/.pi/agent/models.json`를 통해 사용자 정의 공급자 및 모델(Ollama, vLLM, LM Studio, 프록시)을 추가합니다.\n\n## 목차\n\n- [Minimal Example](#minimal-example)\n- [Full Example](#full-example)\n- [Supported APIs](#supported-apis)\n- [Provider Configuration](#provider-configuration)\n- [Model Configuration](#model-configuration)\n- [Overriding Built-in Providers](#overriding-built-in-providers)\n- [Per-model Overrides](#per-model-overrides)\n- [Anthropic Messages Compatibility](#anthropic-messages-compatibility)\n- [OpenAI Compatibility](#openai-compatibility)\n\n## 최소한의 예\n\n로컬 모델(Ollama, LM Studio, vLLM)의 경우 모델당 `id`만 필요합니다.\n\n```json\n{\n  \"providers\": {\n    \"ollama\": {\n      \"baseUrl\": \"http://localhost:11434/v1\",\n      \"api\": \"openai-completions\",\n      \"apiKey\": \"ollama\",\n      \"models\": [\n        { \"id\": \"llama3.1:8b\" },\n        { \"id\": \"qwen2.5-coder:7b\" }\n      ]\n    }\n  }\n}\n```\n\n`apiKey` 값은 Ollama가 이를 무시하기 때문에 자리 표시자입니다. pi는 모델이 `/model`에 나타나기 전에 인증이 필요한 것으로 간주하므로 키가 없는 로컬 서버는 더미 값을 유지하고 `/login`를 사용하여 해당 공급자에 대한 키를 저장하거나 모델을 선택할 때 `--api-key`를 전달해야 합니다.\n\n일부 OpenAI 호환 서버는 추론 가능 모델에 사용되는 `developer` 역할을 이해하지 못합니다. 해당 공급자의 경우 `compat.supportsDeveloperRole`를 `false`로 설정하면 pi가 대신 시스템 프롬프트를 `system` 메시지로 보냅니다. 서버에서도 `reasoning_effort`를 지원하지 않는 경우 `compat.supportsReasoningEffort`도 `false`로 설정하세요.\n\n공급자 수준에서 `compat`를 설정하여 모든 모델에 적용하거나 모델 수준에서 특정 모델을 재정의할 수 있습니다. 이는 일반적으로 Ollama, vLLM, SGLang 및 유사한 OpenAI 호환 서버에 적용됩니다.\n\n```json\n{\n  \"providers\": {\n    \"ollama\": {\n      \"baseUrl\": \"http://localhost:11434/v1\",\n      \"api\": \"openai-completions\",\n      \"apiKey\": \"ollama\",\n      \"compat\": {\n        \"supportsDeveloperRole\": false,\n        \"supportsReasoningEffort\": false\n      },\n      \"models\": [\n        {\n          \"id\": \"gpt-oss:20b\",\n          \"reasoning\": true\n        }\n      ]\n    }\n  }\n}\n```\n\n## 전체 예시\n\n특정 값이 필요할 때 기본값을 재정의합니다.\n\n```json\n{\n  \"providers\": {\n    \"ollama\": {\n      \"baseUrl\": \"http://localhost:11434/v1\",\n      \"api\": \"openai-completions\",\n      \"apiKey\": \"ollama\",\n      \"models\": [\n        {\n          \"id\": \"llama3.1:8b\",\n          \"name\": \"Llama 3.1 8B (Local)\",\n          \"reasoning\": false,\n          \"input\": [\"text\"],\n          \"contextWindow\": 128000,\n          \"maxTokens\": 32000,\n          \"cost\": { \"input\": 0, \"output\": 0, \"cacheRead\": 0, \"cacheWrite\": 0 }\n        }\n      ]\n    }\n  }\n}\n```\n\n`/model`을 열 때마다 파일이 다시 로드됩니다. 세션 중에 편집합니다. 다시 시작할 필요가 없습니다.\n\n## Google AI 스튜디오 예시\n\n맞춤 Gemma 4 항목을 포함하여 Google AI Studio에서 모델을 추가하려면 `google-generative-ai`를 `baseUrl`와 함께 사용하세요.\n\n```json\n{\n  \"providers\": {\n    \"my-google\": {\n      \"baseUrl\": \"https://generativelanguage.googleapis.com/v1beta\",\n      \"api\": \"google-generative-ai\",\n      \"apiKey\": \"$GEMINI_API_KEY\",\n      \"models\": [\n        {\n          \"id\": \"gemma-4-31b-it\",\n          \"name\": \"Gemma 4 31B\",\n          \"input\": [\"text\", \"image\"],\n          \"contextWindow\": 262144,\n          \"reasoning\": true\n        }\n      ]\n    }\n  }\n}\n```\n\n`google-generative-ai` API 유형에 사용자 정의 모델을 추가하는 경우 `baseUrl`가 필요합니다.\n\n## APIs 지원됨\n\n| API | 설명 |\n|-----|-------------|\n| `openai-completions` | OpenAI 채팅 완료(호환성이 가장 높음) |\n| `openai-responses` | OpenAI 응답 API |\n| `anthropic-messages` | 인류학적 메시지 API |\n| `google-generative-ai` | 구글 제너레이티브 AI |\n\n공급자 수준(모든 모델의 기본값) 또는 모델 수준(모델별 재정의)에서 `api`를 설정합니다.\n\n## 공급자 구성\n\n| 필드 | 설명 |\n|-------|-------------|\n| `baseUrl` | API 엔드포인트 URL |\n| `api` | API 유형(위 참조) |\n| `apiKey` | 선택 사항 API key 구성(아래 값 해상도 참조). `/login`/`auth.json` 또는 CLI `--api-key`에서 인증을 제공하는 경우 생략하세요. |\n| `oauth` | 동적 OAuth 공급자 유형. 현재 `\"radius\"`를 지원합니다. 게이트웨이 `baseUrl`가 필요합니다. |\n| `headers` | 맞춤 헤더(아래 값 확인 참조) |\n| `authHeader` | `true`를 설정하면 `Authorization: Bearer <apiKey>`가 자동으로 추가됩니다. |\n| `models` | 모델 구성 배열 |\n| `modelOverrides` | 이 공급자의 내장 또는 확장 등록 모델에 대한 모델별 재정의 |\n\n`models`가 있는 공급자의 경우 기본 제공되지 않는 공급자 구성에는 공급자 또는 모델 수준에서 `baseUrl` 및 `api` 값이 필요합니다. `apiKey`는 파일을 로드할 필요가 없습니다. `/login`/`auth.json`, CLI `--api-key` 또는 공급자 `apiKey`를 통해 인증이 구성되면 모델을 사용할 수 있습니다. 인증이 구성되지 않은 경우 모델이 로드되지만 `/model` 및 `--list-models`에서는 사용할 수 없는 상태로 유지됩니다.\n\n### 가치 결정\n\n`apiKey` 및 `headers` 필드는 명령 실행, 환경 보간 및 리터럴을 지원합니다.\n\n- **쉘 명령:** 시작 시 `\"!command\"` 전체 값을 명령으로 실행하고 stdout을 사용합니다.\n  ```json\n  \"apiKey\": \"!security find-generic-password -ws 'anthropic'\"\n  \"apiKey\": \"!op read 'op://vault/item/credential'\"\n  ```\n- **환경 보간:** `\"$ENV_VAR\"` 또는 `\"${ENV_VAR}\"`는 명명된 변수의 값을 사용합니다. 보간은 더 큰 리터럴 내에서 작동합니다.\n  ```json\n  \"apiKey\": \"$MY_API_KEY\"\n  \"apiKey\": \"${KEY_PREFIX}_${KEY_SUFFIX}\"\n  ```\n  `$FOO_BAR`는 변수 `FOO_BAR`입니다. `BAR`가 리터럴 텍스트인 경우 `${FOO}_BAR`를 사용하세요. 환경 변수가 누락되면 값이 확인되지 않습니다.\n- **이스케이프:** `\"$\"`는 리터럴 `\"$\"`을 내보냅니다. `\"$!\"`는 명령 실행을 트리거하지 않고 리터럴 `\"!\"`을 내보냅니다.\n  ```json\n  \"apiKey\": \"$$literal-dollar-prefix\"\n  \"apiKey\": \"$!literal-bang-prefix\"\n  ```\n- **리터럴 값:** 직접 사용됩니다. `MY_API_KEY`와 같은 일반 대문자 문자열은 리터럴입니다. 환경 변수에는 `$MY_API_KEY`를 사용하세요.\n  ```json\n  \"apiKey\": \"sk-...\"\n  ```\n\n`models.json`의 경우 셸 명령은 요청 시 해결됩니다. pi는 의도적으로 임의 명령에 대해 내장 TTL, 오래된 재사용 또는 복구 논리를 적용하지 않습니다. 명령마다 다른 캐싱 및 실패 전략이 필요하며 pi는 올바른 것을 추론할 수 없습니다.\n\n명령이 느리고, 비용이 많이 들고, 속도가 제한되어 있거나 일시적 오류 시 이전 값을 계속 사용해야 하는 경우 원하는 캐싱 또는 TTL 동작을 구현하는 고유한 스크립트나 명령으로 래핑하세요.\n\n`/model` 가용성 확인은 구성된 인증 존재를 사용하고 셸 명령을 실행하지 않습니다.\n\n### 맞춤 헤더\n\n```json\n{\n  \"providers\": {\n    \"custom-proxy\": {\n      \"baseUrl\": \"https://proxy.example.com/v1\",\n      \"apiKey\": \"$MY_API_KEY\",\n      \"api\": \"anthropic-messages\",\n      \"headers\": {\n        \"x-portkey-api-key\": \"$PORTKEY_API_KEY\",\n        \"x-secret\": \"!op read 'op://vault/item/secret'\"\n      },\n      \"models\": [...]\n    }\n  }\n}\n```\n\n## 모델 구성\n\n| 필드 | 필수의 | 기본 | 설명 |\n|-------|----------|---------|-------------|\n| `id` | 예 | — | 모델 식별자(API로 전달됨) |\n| `name` | 아니요 | `id` | 사람이 읽을 수 있는 모델 라벨. 일치(`--model` 패턴)에 사용되며 보조 모델 세부 텍스트로 표시됩니다. |\n| `api` | 아니요 | 공급자의 `api` | 이 모델에 대한 공급자의 API 재정의 |\n| `reasoning` | 아니요 | `false` | 확장된 사고를 지원합니다 |\n| `thinkingLevelMap` | 아니요 | 생략 | 파이 사고 수준을 공급자 값에 매핑하고 지원되지 않는 수준을 표시합니다(아래 참조). |\n| `input` | 아니요 | `[\"text\"]` | 입력 유형: `[\"text\"]` 또는 `[\"text\", \"image\"]` |\n| `contextWindow` | 아니요 | `128000` | 토큰의 컨텍스트 창 크기 |\n| `maxTokens` | 아니요 | `16384` | 최대 출력 토큰 |\n| `samplingParams` | 아니요 | 생략 | 샘플링 매개변수는 모든 요청 본문에 그대로 병합되었습니다(아래 참조). |\n| `cost` | 아니요 | 모두 0 | 선택적 요청 전체 입력 가격 책정 계층이 포함된 백만 개당 토큰 요율 |\n| `compat` | 아니요 | 제공자 `compat` | 공급자 호환성이 재정의됩니다. 둘 다 설정된 경우 공급자 수준 `compat`과 병합됩니다. |\n\n비용 계층은 전체 대체 요금 세트를 제공하고 총 입력 사용량(`input + cacheRead + cacheWrite`)이 `inputTokensAbove`을 초과하는 경우 전체 요청에 적용됩니다. 여러 계층이 일치하는 경우 가장 높은 임계값이 우선합니다.\n\n```json\n{\n  \"cost\": {\n    \"input\": 5,\n    \"output\": 30,\n    \"cacheRead\": 0.5,\n    \"cacheWrite\": 6.25,\n    \"tiers\": [\n      {\n        \"inputTokensAbove\": 272000,\n        \"input\": 10,\n        \"output\": 45,\n        \"cacheRead\": 1,\n        \"cacheWrite\": 12.5\n      }\n    ]\n  }\n}\n```\n\n현재 동작:\n- `/model`, `--list-models` 및 대화형 바닥글에는 모델 `id`별 항목이 표시됩니다.\n- 구성된 `name`는 모델 일치 및 보조 모델 세부 텍스트에 사용됩니다. 바닥글/상태 표시줄 모델 ID를 대체하지 않습니다.\n\n### 샘플링 매개변수\n\n`samplingParams`는 필드 pi가 자체적으로 설정된 후 모델의 모든 요청 본문에 축어적으로 병합된 자유 형식 개체이므로 해당 키가 승리합니다. 이를 사용하여 pi가 모델링하지 않는 샘플링 매개변수(llama.cpp의 `min_p` 또는 vLLM의 `top_k`와 같은 서버별 매개변수 포함)를 보냅니다.\n\n```json\n{\n  \"id\": \"deepseek-v4-flash\",\n  \"samplingParams\": {\n    \"temperature\": 1.0,\n    \"top_p\": 0.95,\n    \"top_k\": 0,\n    \"min_p\": 0.0\n  }\n}\n```\n\nOpenAI 호환 API에만 적용됩니다(`openai-completions`, `openai-responses`, `azure-openai-responses`). 다른 API는 무시합니다. 키는 pi의 명명된 요청 필드를 재정의하므로(예를 들어 여기서 `temperature` 키는 요청 수준 온도를 능가함) 모델에 대한 샘플링 진실의 단일 소스로 선호합니다. `modelOverrides`에서 `samplingParams`는 키당 기본 모델 값을 병합합니다.\n\n### 사고 수준 지도\n\n모델별 사고 제어를 설명하려면 모델에 `thinkingLevelMap`를 사용하세요. 키는 파이 사고 수준입니다: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. 지도에는 구멍이 있을 수 있습니다. 예를 들어, 모델은 `xhigh`를 노출하지 않고 `high` 및 `max`를 노출할 수 있습니다.\n\n값은 3개 상태입니다.\n\n| 값 | 의미 |\n|-------|---------|\n| 생략 | `high`까지의 표준 수준에서는 공급자의 기본 매핑을 사용합니다. 확장된 `xhigh` 및 `max` 수준은 지원되지 않습니다. |\n| 끈 | 레벨이 지원되며 이 값이 공급자에게 전송됩니다. |\n| `null` | 레벨이 지원되지 않으며 숨겨지거나 건너뛰거나 고정되었습니다. |\n\noff, high, max 추론만 지원하는 모델의 예:\n\n```json\n{\n  \"id\": \"deepseek-v4-pro\",\n  \"reasoning\": true,\n  \"thinkingLevelMap\": {\n    \"minimal\": null,\n    \"low\": null,\n    \"medium\": null,\n    \"high\": \"high\",\n    \"xhigh\": null,\n    \"max\": \"max\"\n  }\n}\n```\n\n사고를 비활성화할 수 없는 모델의 예:\n\n```json\n{\n  \"id\": \"always-thinking-model\",\n  \"reasoning\": true,\n  \"thinkingLevelMap\": {\n    \"off\": null\n  }\n}\n```\n\n마이그레이션: `compat.reasoningEffortMap`를 사용한 이전 구성은 해당 매핑을 모델 수준 `thinkingLevelMap`로 이동해야 합니다. UI에 표시되어서는 안 되는 레벨에는 `null`를 사용하세요.\n\n## 내장 기능 재정의 Providers\n\n모델을 재정의하지 않고 프록시를 통해 내장 공급자를 라우팅합니다.\n\n```json\n{\n  \"providers\": {\n    \"anthropic\": {\n      \"baseUrl\": \"https://my-proxy.example.com/v1\"\n    }\n  }\n}\n```\n\n내장된 모든 Anthropic 모델은 계속 사용할 수 있습니다. 기존 OAuth 또는 API key 인증은 계속 작동합니다.\n\n사용자 정의 모델을 내장 공급자로 병합하려면 `models` 배열을 포함하세요.\n\n```json\n{\n  \"providers\": {\n    \"anthropic\": {\n      \"baseUrl\": \"https://my-proxy.example.com/v1\",\n      \"apiKey\": \"$ANTHROPIC_API_KEY\",\n      \"api\": \"anthropic-messages\",\n      \"models\": [...]\n    }\n  }\n}\n```\n\n병합 의미:\n- 내장 모델이 유지됩니다.\n- 사용자 정의 모델은 공급자 내에서 `id`로 업데이트됩니다.\n- 맞춤 모델 `id`이 기본 제공 모델 `id`과 일치하면 맞춤 모델이 해당 기본 제공 모델을 대체합니다.\n- 맞춤 모델 `id`이 새로운 경우 내장 모델과 함께 추가됩니다.\n\n## 모델별 재정의\n\n`modelOverrides`를 사용하면 공급자의 전체 모델 목록을 교체하지 않고 내장 모델과 일치하는 확장 등록 모델을 맞춤 설정할 수 있습니다.\n\n```json\n{\n  \"providers\": {\n    \"openrouter\": {\n      \"modelOverrides\": {\n        \"anthropic/claude-sonnet-4\": {\n          \"name\": \"Claude Sonnet 4 (Bedrock Route)\",\n          \"compat\": {\n            \"openRouterRouting\": {\n              \"only\": [\"amazon-bedrock\"]\n            }\n          }\n        }\n      }\n    }\n  }\n}\n```\n\n`modelOverrides`는 모델별로 `name`, `reasoning`, `thinkingLevelMap`, `input`, `cost`(일부), `contextWindow`, `maxTokens`, `samplingParams`(키당 병합), `headers`, `compat` 필드를 지원합니다.\n\nDirect OpenAI GPT-5.6 Sol, Terra 및 Luna는 기본적으로 `272000` 컨텍스트 창으로 설정되므로 요청은 OpenAI의 단기 컨텍스트 가격 책정 계층 내에 유지됩니다. OpenAI의 1.05M 컨텍스트 창을 선택하려면 사용하는 각 모델에 대해 이를 늘리십시오.\n\n```json\n{\n  \"providers\": {\n    \"openai\": {\n      \"modelOverrides\": {\n        \"gpt-5.6-sol\": {\n          \"contextWindow\": 1050000\n        }\n      }\n    }\n  }\n}\n```\n\n재정의는 기본 제공 가격 메타데이터를 유지합니다. 총 입력 토큰이 272K를 초과하는 요청은 전체 요청에 대해 GPT-5.6의 긴 컨텍스트 속도를 사용합니다. 필요한 경우 `gpt-5.6-terra` 또는 `gpt-5.6-luna`에 동일한 재정의를 적용합니다.\n\n행동 참고사항:\n- `modelOverrides`는 내장 공급자 모델과 일치하는 확장 프로그램에 등록된 공급자 모델에 적용됩니다.\n- 알 수 없는 모델 ID는 무시됩니다.\n- 공급자 수준 `baseUrl`/`headers`을 `modelOverrides`와 결합할 수 있습니다.\n- `name`를 재정의하면 모델 일치와 보조 세부 텍스트만 변경됩니다. 바닥글과 기본 모델 목록에는 계속해서 모델 `id`이 표시됩니다.\n- 공급자에 대해 `models`도 정의된 경우 기본 제공 재정의 후에 사용자 지정 모델이 병합됩니다. 동일한 `id`을 가진 사용자 정의 모델이 재정의된 내장 모델 항목을 대체합니다.\n\n## 인류학적 메시지 호환성\n\n`api: \"anthropic-messages\"`를 사용하는 공급자나 프록시의 경우 `compat`를 사용하여 Anthropic 관련 요청 호환성을 제어합니다.\n\n기본적으로 pi는 도구별 `eager_input_streaming: true`를 보냅니다. 프록시 또는 Anthropic 호환 백엔드가 해당 필드를 거부하는 경우 `supportsEagerToolInputStreaming`를 `false`로 설정하세요. Pi는 `tools[].eager_input_streaming`를 생략하고 대신 도구 사용 요청에 대한 레거시 `fine-grained-tool-streaming-2025-05-14` 베타 헤더를 ​​보냅니다.\n\n일부 인류 모델에는 기존 예산 기반 사고 페이로드 대신 적응형 사고(`thinking.type: \"adaptive\"` + `output_config.effort`)가 필요합니다. 내장 모델에서는 이를 자동으로 설정합니다. 해당 모델로 라우팅되는 사용자 지정 공급자 또는 별칭의 경우 `forceAdaptiveThinking`를 `true`로 설정하세요.\n\n일부 Anthropic 호환 제공자는 빈 서명이 있는 사고 블록을 방출하고 여전히 재생될 것으로 기대합니다. 해당 제공업체에 대해서만 `allowEmptySignature`를 `true`로 설정하세요. 진정한 인류학은 공허한 사고의 서명을 거부합니다.\n\n내장된 인류 모델은 모델 메타데이터에서 `supportsStrictTools`를 활성화합니다. 사용자 정의 Anthropic 호환 모델은 해당 엔드포인트가 엄격한 JSON 스키마 도구 정의를 허용하는 경우 이를 `true`로 설정해야 합니다.\n\n```json\n{\n  \"providers\": {\n    \"anthropic-proxy\": {\n      \"baseUrl\": \"https://proxy.example.com\",\n      \"api\": \"anthropic-messages\",\n      \"apiKey\": \"$ANTHROPIC_PROXY_KEY\",\n      \"compat\": {\n        \"supportsEagerToolInputStreaming\": false,\n        \"supportsLongCacheRetention\": true,\n        \"forceAdaptiveThinking\": true,\n        \"allowEmptySignature\": true\n      },\n      \"models\": [\n        {\n          \"id\": \"claude-opus-4-7\",\n          \"reasoning\": true,\n          \"input\": [\"text\", \"image\"]\n        }\n      ]\n    }\n  }\n}\n```\n\n| 필드 | 설명 |\n|-------|-------------|\n| `supportsEagerToolInputStreaming` | 제공자가 도구별 `eager_input_streaming`를 허용하는지 여부. 기본값: `true`. 해당 필드를 생략하고 도구 지원 요청에 대해 기존의 세분화된 도구 스트리밍 베타 헤더를 ​​사용하려면 `false`로 설정합니다. |\n| `supportsLongCacheRetention` | 캐시 보존이 `long`일 때 공급자가 Anthropic 장기 캐시 보존(`cache_control.ttl: \"1h\"`)을 허용하는지 여부입니다. 기본값: `true`. |\n| `sendSessionAffinityHeaders` | 캐싱이 활성화된 경우 세션 ID에서 `x-session-affinity`를 보낼지 여부입니다. 기본값: 알려진 공급자에 대해 자동 감지됩니다. |\n| `supportsCacheControlOnTools` | 제공자가 도구 정의에서 Anthropic 스타일 `cache_control` 마커를 허용하는지 여부입니다. 기본값: `true`. |\n| `forceAdaptiveThinking` | 이 모델에 대해 적응적 사고(`thinking.type: \"adaptive\"` + `output_config.effort`)를 보낼지 여부입니다. 내장된 적응형 모델은 이를 자동으로 설정합니다. 기본값: `false`. |\n| `allowEmptySignature` | 생각을 텍스트로 변환하는 대신 빈 생각 서명을 `signature: \"\"`로 재생할지 여부입니다. 기본값: `false`. |\n| `supportsStrictTools` | 공급자가 엄격한 JSON 스키마 도구 정의를 허용하는지 여부입니다. 기본값: `false`; 내장된 인류 모델은 생성된 메타데이터에서 이를 가능하게 합니다. |\n\n## OpenAI 호환성\n\n부분적인 OpenAI 호환성을 갖춘 공급자의 경우 `compat` 필드를 사용하세요.\n\n- 공급자 수준 `compat`은 해당 공급자 아래의 모든 모델에 기본값을 적용합니다.\n- 모델 수준 `compat`은 해당 모델의 공급자 수준 값을 재정의합니다.\n\n```json\n{\n  \"providers\": {\n    \"local-llm\": {\n      \"baseUrl\": \"http://localhost:8080/v1\",\n      \"api\": \"openai-completions\",\n      \"compat\": {\n        \"supportsUsageInStreaming\": false,\n        \"maxTokensField\": \"max_tokens\"\n      },\n      \"models\": [...]\n    }\n  }\n}\n```\n\n| 필드 | 설명 |\n|-------|-------------|\n| `supportsStore` | 공급자는 `store` 필드를 지원합니다. |\n| `supportsDeveloperRole` | `developer` 대 `system` 역할 사용 |\n| `supportsReasoningEffort` | `reasoning_effort` 매개변수 지원 |\n| `supportsUsageInStreaming` | `stream_options: { include_usage: true }` 지원 (기본값: `true`) |\n| `supportsFinishReason` | 스트리밍 응답에 `finish_reason`가 포함되는지 여부. `false`인 경우 pi는 스트림이 끝나면 `stop` 또는 `toolUse`를 추론합니다. 기본값: `true`. |\n| `maxTokensField` | `max_completion_tokens` 또는 `max_tokens`를 사용하세요. |\n| `requiresToolResultName` | 도구 결과 메시지에 `name` 포함 |\n| `requiresAssistantAfterToolResult` | 도구 결과 후 사용자 메시지 앞에 보조 메시지 삽입 |\n| `requiresThinkingAsText` | 사고 블록을 일반 텍스트로 변환 |\n| `requiresReasoningContentOnAssistantMessages` | 추론이 활성화된 경우 재생된 모든 보조 메시지에 빈 `reasoning_content` 포함 |\n| `thinkingFormat` | `reasoning_effort`, `openrouter`, `deepseek`, `together`, `baseten`, `zai`, `qwen`, `chat-template` 또는 `qwen-chat-template` 사고 매개변수를 사용하세요. |\n| `chatTemplateKwargs` | `thinkingFormat: \"chat-template\"`의 `chat_template_kwargs` 값; 파이 제어 사고 값에는 `{ \"$var\": \"thinking.enabled\" }` 또는 `{ \"$var\": \"thinking.effort\" }`를 사용하세요. |\n| `chatTemplateArgs` | `thinkingFormat: \"baseten\"`의 `chat_template_args` 값; 파이 제어 사고 값에는 `{ \"$var\": \"thinking.enabled\" }` 또는 `{ \"$var\": \"thinking.effort\" }`를 사용하세요. |\n| `cacheControlFormat` | 시스템 프롬프트, 마지막 도구 정의, 마지막 사용자, 보조자 또는 도구 결과 텍스트 콘텐츠에 인류 스타일 `cache_control` 마커를 사용하세요. 현재는 `anthropic`만 지원됩니다. |\n| `sendSessionAffinityHeaders` | `openai-completions`의 경우 캐싱이 활성화되면 세션 ID에서 세션 선호도 헤더를 보냅니다. 기본값: `false`. |\n| `sessionAffinityFormat` | `openai-completions` 및 `openai-responses`의 경우 세션 선호도 헤더 형식: `openai`는 `session_id`/`x-client-request-id`를 보냅니다(완료도 `x-session-affinity`). `openai-nosession`는 밑줄이 포함된 `session_id` 헤더를 생략하고, `openrouter`는 `x-session-id`를 보냅니다. `prompt_cache_key` 본문 매개변수에는 영향을 주지 않습니다. 기본값: 자동 감지. |\n| `supportsStrictMode` | 공급자가 엄격한 JSON-스키마 기능 도구 정의를 허용하는지 여부입니다. 기본값은 API에 따라 다릅니다. 내장된 OpenAI 모델은 명시적인 기능 메타데이터를 전달합니다. |\n| `supportsOpenAIGrammarTools` | OpenAI 호환 API가 맞춤형 Lark/정규식 문법 도구를 내보낼지 여부입니다. `false`인 경우 문법이 제한된 도구는 일반 기능 도구로 대체됩니다. 기본값: `false`; 내장된 모델 카탈로그를 사용하면 OpenAI, OpenAI Codex, Azure OpenAI, GitHub Copilot, opencode 및 Cloudflare AI Gateway의 GPT-5+ 모델에 사용할 수 있습니다. |\n| `deferredToolsMode` | 공급자별 지연 도구 직렬화를 사용합니다. 현재 Kimi의 OpenAI 호환 채팅 완료 형식에는 `\"kimi\"`만 지원됩니다. |\n| `supportsLongCacheRetention` | 캐시 보존이 `long`일 때 공급자가 긴 캐시 보존을 허용하는지 여부: OpenAI 프롬프트 캐싱의 경우 `prompt_cache_retention: \"24h\"` 또는 `cacheControlFormat`이 `anthropic`일 때 `cache_control.ttl: \"1h\"`. 기본값: `true`. |\n| `openRouterRouting` | OpenRouter 공급자 라우팅 기본 설정. 이 개체는 [OpenRouter API request](https://openrouter.ai/docs/guides/routing/provider-selection)의 `provider` 필드에 있는 그대로 전송됩니다. |\n| `vercelGatewayRouting` | 공급자 선택을 위한 Vercel AI Gateway 라우팅 구성(`only`, `order`) |\n\n`openrouter`는 `reasoning: { effort }`을 사용합니다. `together`는 `reasoning: { enabled }`를 사용하고 `supportsReasoningEffort`가 활성화되면 `reasoning_effort`도 사용합니다. `qwen`는 최상위 수준 `enable_thinking`을 사용합니다. `chat_template_kwargs.enable_thinking` 및 `preserve_thinking`가 필요한 로컬 Qwen 호환 서버에는 `qwen-chat-template`를 사용하세요. DeepSeek V3.x 템플릿의 경우 `chatTemplateKwargs: { \"thinking\": { \"$var\": \"thinking.enabled\" } }`와 같이 구성 가능한 `chat_template_kwargs`가 필요한 vLLM/Hugging Face 채팅 템플릿에는 `chat-template`을 사용하세요. `chat_template_args`을 통해 토글 컨트롤을 노출하고 선택적으로 최상위 `reasoning_effort`를 지원하는 공급자의 경우 `thinkingFormat: \"baseten\"`를 `chatTemplateArgs`와 함께 사용하세요.\n\n`cacheControlFormat: \"anthropic\"`는 텍스트 콘텐츠 및 도구 정의의 `cache_control` 마커를 통해 Anthropic 스타일 프롬프트 캐싱을 노출하는 OpenAI 호환 공급자용입니다.\n\n예:\n\n```json\n{\n  \"providers\": {\n    \"openrouter\": {\n      \"baseUrl\": \"https://openrouter.ai/api/v1\",\n      \"apiKey\": \"$OPENROUTER_API_KEY\",\n      \"api\": \"openai-completions\",\n      \"models\": [\n        {\n          \"id\": \"openrouter/anthropic/claude-3.5-sonnet\",\n          \"name\": \"OpenRouter Claude 3.5 Sonnet\",\n          \"compat\": {\n            \"openRouterRouting\": {\n              \"allow_fallbacks\": true,\n              \"require_parameters\": false,\n              \"data_collection\": \"deny\",\n              \"zdr\": true,\n              \"enforce_distillable_text\": false,\n              \"order\": [\"anthropic\", \"amazon-bedrock\", \"google-vertex\"],\n              \"only\": [\"anthropic\", \"amazon-bedrock\"],\n              \"ignore\": [\"gmicloud\", \"friendli\"],\n              \"quantizations\": [\"fp16\", \"bf16\"],\n              \"sort\": {\n                \"by\": \"price\",\n                \"partition\": \"model\"\n              },\n              \"max_price\": {\n                \"prompt\": 10,\n                \"completion\": 20\n              },\n              \"preferred_min_throughput\": {\n                \"p50\": 100,\n                \"p90\": 50\n              },\n              \"preferred_max_latency\": {\n                \"p50\": 1,\n                \"p90\": 3,\n                \"p99\": 5\n              }\n            }\n          }\n        }\n      ]\n    }\n  }\n}\n```\n\nVercel AI 게이트웨이 예시:\n\n```json\n{\n  \"providers\": {\n    \"vercel-ai-gateway\": {\n      \"baseUrl\": \"https://ai-gateway.vercel.sh/v1\",\n      \"apiKey\": \"$AI_GATEWAY_API_KEY\",\n      \"api\": \"openai-completions\",\n      \"models\": [\n        {\n          \"id\": \"moonshotai/kimi-k2.5\",\n          \"name\": \"Kimi K2.5 (Fireworks via Vercel)\",\n          \"reasoning\": true,\n          \"input\": [\"text\", \"image\"],\n          \"cost\": { \"input\": 0.6, \"output\": 3, \"cacheRead\": 0, \"cacheWrite\": 0 },\n          \"contextWindow\": 262144,\n          \"maxTokens\": 262144,\n          \"compat\": {\n            \"vercelGatewayRouting\": {\n              \"only\": [\"fireworks\", \"novita\"],\n              \"order\": [\"fireworks\", \"novita\"]\n            }\n          }\n        }\n      ]\n    }\n  }\n}\n```","sourceFile":"models.md"},"packages":{"title":"Pi Packages","markdown":"> pi는 pi 패키지를 만드는 데 도움이 될 수 있습니다. 확장 프로그램, 기술, prompt templates 또는 테마를 번들로 묶어달라고 요청하세요.\n\n\nPi 패키지에는 확장 프로그램, 기술, prompt templates 및 테마가 번들로 포함되어 있어 npm 또는 git을 통해 공유할 수 있습니다. 패키지는 `pi` 키 아래의 `package.json`에서 리소스를 선언하거나 기존 디렉터리를 사용할 수 있습니다.\n\n## 목차\n\n- [Install and Manage](#install-and-manage)\n- [Package Sources](#package-sources)\n- [Creating a Pi Package](#creating-a-pi-package)\n- [Package Structure](#package-structure)\n- [Dependencies](#dependencies)\n- [Package Filtering](#package-filtering)\n- [Enable and Disable Resources](#enable-and-disable-resources)\n- [Scope and Deduplication](#scope-and-deduplication)\n\n## 설치 및 관리\n\n> **보안:** Pi 패키지는 전체 시스템 액세스 권한으로 실행됩니다. Extensions 임의의 코드를 실행하면 기술이 실행 파일 실행을 포함한 모든 작업을 수행하도록 모델에 지시할 수 있습니다. 타사 패키지를 설치하기 전에 소스 코드를 검토하세요.\n\n```bash\npi install npm:@foo/bar@1.0.0\npi install git:github.com/user/repo@v1\npi install https://github.com/user/repo  # raw URLs work too\npi install /absolute/path/to/package\npi install ./relative/path/to/package\n\npi remove npm:@foo/bar\npi list                     # show installed packages from settings\npi update                   # update pi only\npi update --all             # update pi, update packages, and reconcile pinned git refs\npi update --extensions      # update packages and reconcile pinned git refs only\npi update --models          # refresh model catalogs only\npi update --self            # update pi only\npi update --self --force    # reinstall pi even if current\npi update npm:@foo/bar      # update one package\npi update --extension npm:@foo/bar\n```\n\n이 명령은 pi 패키지를 관리하고 `pi update`는 pi CLI 설치를 업데이트할 수 있습니다. pi 자체를 제거하려면 [Quickstart](quickstart.md#uninstall)를 참조하세요.\n\n기본적으로 `install` 및 `remove`는 사용자 설정(`~/.pi/agent/settings.json`)에 기록됩니다. 대신 `-l`를 사용하여 프로젝트 설정(`.pi/settings.json`)에 쓰세요. 프로젝트 설정은 팀과 공유할 수 있으며, pi는 프로젝트를 신뢰한 후 시작 시 누락된 패키지를 자동으로 설치합니다.\n\n패키지를 설치하지 않고 사용해 보려면 `--extension` 또는 `-e`를 사용하세요. 이는 현재 실행을 위한 임시 디렉토리에만 설치됩니다.\n\n```bash\npi -e npm:@foo/bar\npi -e git:github.com/user/repo\n```\n\n## 패키지 소스\n\nPi는 설정에서 세 가지 소스 유형을 허용하고 `pi install`는 허용합니다.\n\n### npm\n\n```\nnpm:@scope/pkg@1.2.3\nnpm:pkg\n```\n\n- 버전이 지정된 사양은 패키지 업데이트(`pi update --extensions`, `pi update --all`)에 의해 고정되고 건너뜁니다.\n- 사용자 설치는 `~/.pi/agent/npm/` 아래에 있습니다.\n- 프로젝트 설치는 `.pi/npm/` 아래에 있습니다.\n- `settings.json`에서 `npmCommand`를 설정하여 npm 패키지 조회를 고정하고 작업을 `mise` 또는 `asdf`와 같은 특정 래퍼 명령에 설치합니다.\n\n예:\n\n```json\n{\n  \"npmCommand\": [\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"]\n}\n```\n\n### 자식\n\n```\ngit:github.com/user/repo@v1\ngit:git@github.com:user/repo@v1\nhttps://github.com/user/repo@v1\nssh://git@github.com/user/repo@v1\n```\n\n- `git:` 접두사가 없으면 프로토콜 URL만 허용됩니다(`https://`, `http://`, `ssh://`, `git://`).\n- `git:` 접두사를 사용하면 `github.com/user/repo` 및 `git@github.com:user/repo`를 포함한 단축 형식이 허용됩니다.\n- HTTPS 및 SSH URL이 모두 지원됩니다.\n- SSH URL은 구성된 SSH 키를 자동으로 사용합니다(`~/.ssh/config` 참조).\n- 비대화형 실행(예: CI)의 경우 `GIT_TERMINAL_PROMPT=0`를 설정하여 자격 증명 프롬프트를 비활성화하고 `GIT_SSH_COMMAND`(예: `ssh -o BatchMode=yes -o ConnectTimeout=5`)을 설정하여 빠르게 실패하도록 설정할 수 있습니다.\n- 참조는 고정된 태그 또는 커밋입니다. `pi update --extensions` 및 `pi update --all`는 최신 참조로 이동하지 않지만 기존 복제본을 구성된 참조로 조정합니다.\n- `pi install git:host/user/repo@new-ref`를 사용하여 설정을 업데이트하고 기존 패키지를 새로 고정된 참조로 이동하세요.\n- `~/.pi/agent/git/<host>/<path>`(글로벌) 또는 `.pi/git/<host>/<path>`(프로젝트)에 복제되었습니다.\n- 조정으로 인해 체크아웃이 변경되면 pi는 복제본을 재설정하고 정리한 다음 `package.json`가 존재하는 경우 `npm install`를 실행합니다.\n\n**SSH 예:**\n```bash\n# git@host:path shorthand (requires git: prefix)\npi install git:git@github.com:user/repo\n\n# ssh:// protocol format\npi install ssh://git@github.com/user/repo\n\n# With version ref\npi install git:git@github.com:user/repo@v1.0.0\n```\n\n### 로컬 경로\n\n```\n/absolute/path/to/package\n./relative/path/to/package\n```\n\n로컬 경로는 디스크의 파일이나 디렉터리를 가리키며 복사하지 않고 설정에 추가됩니다. 상대 경로는 해당 경로가 나타나는 설정 파일을 기준으로 확인됩니다. 경로가 파일인 경우 단일 확장자로 로드됩니다. 디렉터리인 경우 pi는 패키지 규칙을 사용하여 리소스를 로드합니다.\n\n## Pi 패키지 만들기\n\n`pi` 매니페스트를 `package.json`에 추가하거나 기존 디렉터리를 사용하세요. 검색 가능성을 위해 `pi-package` 키워드를 포함하세요.\n\n```json\n{\n  \"name\": \"my-package\",\n  \"keywords\": [\"pi-package\"],\n  \"pi\": {\n    \"extensions\": [\"./extensions\"],\n    \"skills\": [\"./skills\"],\n    \"prompts\": [\"./prompts\"],\n    \"themes\": [\"./themes\"]\n  }\n}\n```\n\n경로는 패키지 루트를 기준으로 합니다. 배열은 glob 패턴과 `!exclusions`을 지원합니다.\n\n### 갤러리 메타데이터\n\n[package gallery](https://pi.dev/packages)는 `pi-package` 태그가 붙은 패키지를 표시합니다. 미리보기를 표시하려면 `video` 또는 `image` 필드를 추가하세요.\n\n```json\n{\n  \"name\": \"my-package\",\n  \"keywords\": [\"pi-package\"],\n  \"pi\": {\n    \"extensions\": [\"./extensions\"],\n    \"video\": \"https://example.com/demo.mp4\",\n    \"image\": \"https://example.com/screenshot.png\"\n  }\n}\n```\n\n- **동영상**: MP4 전용. 데스크톱에서는 마우스를 올리면 자동 재생됩니다. 클릭하면 전체 화면 플레이어가 열립니다.\n- **이미지**: PNG, JPEG, GIF 또는 WebP. 정적 미리보기로 표시됩니다.\n\n둘 다 설정된 경우 비디오가 우선 적용됩니다.\n\n## 패키지 구조\n\n### 컨벤션 디렉토리\n\n`pi` 매니페스트가 없으면 pi는 다음 디렉터리에서 리소스를 자동 검색합니다.\n\n- `extensions/` `.ts` 및 `.js` 파일을 로드합니다.\n- `skills/` `SKILL.md` 폴더를 반복적으로 찾아 최상위 `.md` 파일을 스킬로 로드합니다.\n- `prompts/` `.md` 파일 로드\n- `themes/` `.json` 파일 로드\n\n## 종속성\n\n타사 런타임 종속성은 `package.json`의 `dependencies`에 속합니다. 확장, 스킬, prompt templates 또는 테마를 등록하지 않는 종속성도 `dependencies`에 속합니다. pi가 npm 또는 git에서 패키지를 설치하면 `npm install`가 실행되므로 해당 종속성이 자동으로 설치됩니다.\n\nPi 확장 기능과 기술을 위한 핵심 패키지를 번들로 제공합니다. 이들 중 하나를 가져오는 경우 `peerDependencies`에 `\"*\"` 범위로 나열하고 묶지 마세요: `@earendil-works/pi-ai`, `@earendil-works/pi-agent-core`, `@earendil-works/pi-coding-agent`, `@earendil-works/pi-tui`, `typebox`.\n\n다른 pi 패키지는 tarball에 번들로 포함되어야 합니다. `dependencies` 및 `bundledDependencies`에 추가한 다음 `node_modules/` 경로를 통해 리소스를 참조합니다. Pi 별도의 모듈 루트로 패키지를 로드하므로 별도의 설치로 인해 모듈이 충돌하거나 공유되지 않습니다.\n\n예:\n\n```json\n{\n  \"dependencies\": {\n    \"shitty-extensions\": \"^1.0.1\"\n  },\n  \"bundledDependencies\": [\"shitty-extensions\"],\n  \"pi\": {\n    \"extensions\": [\"extensions\", \"node_modules/shitty-extensions/extensions\"],\n    \"skills\": [\"skills\", \"node_modules/shitty-extensions/skills\"]\n  }\n}\n```\n\n## 패키지 필터링\n\n설정에서 개체 양식을 사용하여 패키지가 로드하는 항목을 필터링합니다.\n\n```json\n{\n  \"packages\": [\n    \"npm:simple-pkg\",\n    {\n      \"source\": \"npm:my-package\",\n      \"extensions\": [\"extensions/*.ts\", \"!extensions/legacy.ts\"],\n      \"skills\": [],\n      \"prompts\": [\"prompts/review.md\"],\n      \"themes\": [\"+themes/legacy.json\"]\n    }\n  ]\n}\n```\n\n`+path` 및 `-path`은 패키지 루트를 기준으로 한 정확한 경로입니다.\n\n- 해당 유형을 모두 로드하려면 키를 생략하세요.\n- 해당 유형을 로드하지 않으려면 `[]`를 사용하세요.\n- `!pattern` 일치하는 항목은 제외됩니다.\n- `+path` 강제로 정확한 경로를 포함합니다.\n- `-path` 정확한 경로를 강제로 제외합니다.\n- 매니페스트 위에 필터 레이어가 있습니다. 이미 허용된 항목의 범위를 좁힙니다.\n\n## 리소스 활성화 및 비활성화\n\n`pi config`를 사용하여 확장 프로그램, 기술, prompt templates 및 설치된 패키지와 로컬 디렉터리의 테마를 활성화하거나 비활성화합니다. `pi config` 전역 설정에서 시작합니다(`~/.pi/agent/settings.json`). 전역 모드와 프로젝트-로컬 모드 사이를 전환하려면 Tab 키를 누르세요. 상속된 전역 리소스가 흐리게 표시된 프로젝트 재정의(`.pi/settings.json`)를 시작하려면 `pi config -l`를 사용하세요.\n\n## 범위 및 중복 제거\n\n패키지는 전역 설정과 프로젝트 설정 모두에 나타날 수 있습니다. 동일한 패키지가 둘 다에 나타나는 경우 프로젝트 항목에 `autoload: false`이 없으면 프로젝트 항목이 우선합니다. 이 경우 전역 항목에 대한 델타로 적용됩니다. 신원은 다음에 의해 결정됩니다.\n\n- npm: 패키지 이름\n- git: 참조가 없는 저장소 URL\n- 로컬: 확인된 절대 경로","sourceFile":"packages.md"},"prompt-templates":{"title":"프롬프트 템플릿","markdown":"> pi는 prompt templates을 생성할 수 있습니다. 귀하의 작업 흐름을 위한 하나를 구축하도록 요청하십시오.\n\n\n프롬프트 템플릿은 전체 프롬프트로 확장되는 Markdown 스니펫입니다. 템플릿을 호출하려면 편집기에 `/name`를 입력하세요. 여기서 `name`는 `.md`가 없는 파일 이름입니다.\n\n## 위치\n\nPi는 다음에서 prompt templates을 로드합니다.\n\n- 글로벌: `~/.pi/agent/prompts/*.md`\n- 프로젝트: `.pi/prompts/*.md` (프로젝트를 신뢰한 후에만)\n- 패키지: `prompts/` 디렉토리 또는 `package.json`의 `pi.prompts` 항목\n- 설정: `prompts` 파일 또는 디렉터리가 포함된 배열\n- CLI: `--prompt-template <path>` (반복 가능)\n\n`--no-prompt-templates`로 검색을 비활성화하세요.\n\n## 체재\n\n```markdown\n---\ndescription: Review staged git changes\n---\nReview the staged changes (`git diff --cached`). Focus on:\n- Bugs and logic errors\n- Security issues\n- Error handling gaps\n```\n\n- 파일 이름이 명령 이름이 됩니다. `review.md`는 `/review`이 됩니다.\n- `description`는 선택사항입니다. 누락된 경우 비어 있지 않은 첫 번째 줄이 사용됩니다.\n- `argument-hint`는 선택사항입니다. 설정하면 자동 완성 드롭다운의 설명 앞에 힌트가 표시됩니다.\n\n### 인수 힌트\n\n자동 완성에 예상되는 인수를 표시하려면 머리말에 `argument-hint`를 사용하세요. 필수 인수에는 `<angle brackets>`를 사용하고 선택 인수에는 `[square brackets]`를 사용하세요.\n\n```markdown\n---\ndescription: Review PRs from URLs with structured issue and code analysis\nargument-hint: \"<PR-URL>\"\n---\n```\n\n이는 자동 완성 드롭다운에서 다음과 같이 렌더링됩니다.\n\n```\n→ pr   <PR-URL>       — Review PRs from URLs with structured issue and code analysis\n  is   <issue>        — Analyze GitHub issues (bugs or feature requests)\n  wr   [instructions] — Finish the current task end-to-end\n  cl   — Audit changelog entries before release\n```\n\n## 용법\n\n편집기에서 `/` 뒤에 템플릿 이름을 입력하세요. 자동 완성에는 설명과 함께 사용 가능한 템플릿이 표시됩니다.\n\n```\n/review                           # Expands review.md\n/component Button                 # Expands with argument\n/component Button \"click handler\" # Multiple arguments\n```\n\n## 인수\n\n템플릿은 위치 인수, 기본값 및 단순 슬라이싱을 지원합니다.\n\n- `$1`, `$2`,... 위치 인수\n- `$@` 또는 `$ARGUMENTS` 모든 인수가 결합된 경우\n- `${1:-default}` 존재하거나 비어 있지 않으면 인수 1을 사용하고, 그렇지 않으면 `default`\n- `${@:-default}` 또는 `${ARGUMENTS:-default}`는 존재하거나 비어 있지 않은 경우 모든 인수를 사용하고, 그렇지 않은 경우 `default`\n- `${@:N}` N번째 위치의 인수인 경우(1-인덱스)\n- N에서 시작하는 `L` 인수의 경우 `${@:N:L}`\n\n예:\n\n```markdown\n---\ndescription: Create a component\n---\nCreate a React component named $1 with features: $@\n```\n\n기본값은 선택적 인수에 유용합니다.\n\n```markdown\nSummarize the current state in ${1:-7} bullet points.\n```\n\n사용법: `/component Button \"onClick handler\" \"disabled support\"`\n\n## 로딩 규칙\n\n- `prompts/`의 템플릿 검색은 비재귀적입니다.\n- 하위 디렉터리에 템플릿을 추가하려면 `prompts` 설정이나 패키지 매니페스트를 통해 명시적으로 추가하세요.","sourceFile":"prompt-templates.md"},"providers":{"title":"Providers","markdown":"Pi는 환경 변수 또는 인증 파일을 통해 OAuth 및 API key 공급자를 통해 구독 기반 공급자를 지원합니다. 내장 카탈로그는 pi와 함께 제공됩니다. 구성된 공급자는 최신 카탈로그를 새로 고치고 오프라인 사용을 위해 `~/.pi/agent/models-store.json`에 캐시할 수 있습니다.\n\n## 목차\n\n- [Subscriptions](#subscriptions)\n- [API Keys](#api-keys)\n- [Auth File](#auth-file)\n- [Cloud Providers](#cloud-providers)\n- [llama.cpp](#llamacpp)\n- [Custom Providers](#custom-providers)\n- [Resolution Order](#resolution-order)\n\n## 구독\n\n대화형 모드에서 `/login`를 사용한 후 제공업체를 선택하세요.\n\n- ChatGPT Plus/Pro(코덱스)\n- 클로드 프로/맥스\n- GitHub 부조종사\n- xAI(Grok/X 구독)\n- OpenRouter(OAuth 발행 API key OpenRouter 크레딧으로 청구)\n- 반지름\n\n자격 증명을 삭제하려면 `/logout`를 사용하세요. 토큰은 `~/.pi/agent/auth.json`에 저장되며 만료되면 자동으로 새로 고쳐집니다. 대신 OpenRouter는 자동으로 만료되지 않는 사용자 제어 API key를 생성합니다.\n\n### 오픈AI 코덱스\n\n- ChatGPT Plus 또는 Pro 구독 필요\n- OpenAI가 공식적으로 승인함: [Codex for OSS](https://developers.openai.com/community/codex-for-oss)\n\n### 클로드 프로/맥스\n\nClaude Pro/Max 계정에 대해 Anthropic 구독 인증이 활성화되어 있습니다. 제3자 하네스 사용량은 [extra usage](https://claude.ai/settings/usage)부터이며 Claude 요금제 제한이 아닌 토큰별로 요금이 청구됩니다.\n\n### GitHub 부조종사\n\n- github.com의 경우 Enter 키를 누르거나 GitHub Enterprise Server 도메인을 입력하세요.\n- \"모델이 지원되지 않음\"이 표시되면 VS Code에서 활성화합니다: Copilot Chat → 모델 선택기 → 모델 선택 → \"활성화\"\n\n### xAI(Grok/X 구독)\n\n- `/login xai`를 실행한 다음 **구독 사용**을 선택하세요.\n- `XAI_API_KEY`는 **API key** 사용을 통해 계속 사용할 수 있습니다.\n\n### 오픈라우터\n\n- `/login openrouter`를 실행한 다음 **OpenRouter로 로그인**을 선택하여 OpenRouter PKCE 인증 흐름을 엽니다.\n- 승인을 통해 OpenRouter 크레딧으로 청구되는 사용자 제어 OpenRouter API key가 생성됩니다.\n- 원격/헤드리스 시스템(예: SSH 이상)에서 브라우저는 루프백 콜백에 도달할 수 없습니다. 대신 최종 리디렉션 URL(또는 인증 코드)을 로그인 프롬프트에 붙여넣으세요.\n- `OPENROUTER_API_KEY`는 **API key** 사용을 통해 계속 사용할 수 있습니다.\n\n### 반지름\n\nRadius는 동적 `pi-messages` 게이트웨이입니다. `/login radius`는 `auth.json`에 OAuth 토큰을 저장합니다. 게이트웨이 카탈로그는 독립적으로 새로 고쳐지고 `models-store.json`에 캐시됩니다. 맞춤 반경 게이트웨이는 `models.json`에서 `\"oauth\": \"radius\"` 및 게이트웨이 `baseUrl`로 선언할 수 있습니다.\n\n## API 열쇠\n\n### 환경 변수 또는 인증 파일\n\n대화형 모드에서 `/login`를 사용하고 공급자를 선택하여 API key를 `auth.json`에 저장하거나 환경 변수를 통해 자격 증명을 설정하세요.\n\n```bash\nexport ANTHROPIC_API_KEY=sk-ant-...\npi\n```\n\n| 공급자 | 환경변수 | `auth.json` 키 |\n|----------|----------------------|------------------|\n| 인류학 | `ANTHROPIC_API_KEY` | `anthropic` |\n| 앤트 링 | `ANT_LING_API_KEY` | `ant-ling` |\n| Azure OpenAI 응답 | `AZURE_OPENAI_API_KEY` | `azure-openai-responses` |\n| 오픈AI | `OPENAI_API_KEY` | `openai` |\n| DeepSeek | `DEEPSEEK_API_KEY` | `deepseek` |\n| 엔비디아 NIM | `NVIDIA_API_KEY` | `nvidia` |\n| 구글 제미니 | `GEMINI_API_KEY` | `google` |\n| 아마존 기반암 | `AWS_BEARER_TOKEN_BEDROCK` | `amazon-bedrock` |\n| 미스트랄 | `MISTRAL_API_KEY` | `mistral` |\n| 그로크 | `GROQ_API_KEY` | `groq` |\n| 대뇌 | `CEREBRAS_API_KEY` | `cerebras` |\n| Cloudflare AI 게이트웨이 | `CLOUDFLARE_API_KEY` (+ `CLOUDFLARE_ACCOUNT_ID`, `CLOUDFLARE_GATEWAY_ID`) | `cloudflare-ai-gateway` |\n| Cloudflare 작업자 AI | `CLOUDFLARE_API_KEY` (+ `CLOUDFLARE_ACCOUNT_ID`) | `cloudflare-workers-ai` |\n| xAI | `XAI_API_KEY` | `xai` |\n| 오픈라우터 | `OPENROUTER_API_KEY` | `openrouter` |\n| Vercel AI 게이트웨이 | `AI_GATEWAY_API_KEY` | `vercel-ai-gateway` |\n| ZAI 코딩 계획(글로벌) | `ZAI_API_KEY` | `zai` |\n| ZAI 코딩 계획(중국) | `ZAI_CODING_CN_API_KEY` | `zai-coding-cn` |\n| 오픈코드 젠 | `OPENCODE_API_KEY` | `opencode` |\n| 오픈코드 고 | `OPENCODE_API_KEY` | `opencode-go` |\n| 반지름 | `RADIUS_API_KEY` | `radius` |\n| Hugging Face | `HF_TOKEN` | `huggingface` |\n| 불꽃 | `FIREWORKS_API_KEY` | `fireworks` |\n| 함께하는 AI | `TOGETHER_API_KEY` | `together` |\n| 바스텐 | `BASETEN_API_KEY` | `baseten` |\n| 코딩을 위한 키미 | `KIMI_API_KEY` | `kimi-coding` |\n| 미니맥스 | `MINIMAX_API_KEY` | `minimax` |\n| 미니맥스(중국) | `MINIMAX_CN_API_KEY` | `minimax-cn` |\n| Qwen 토큰 플랜(기존 카탈로그) | `QWEN_TOKEN_PLAN_API_KEY` | `qwen-token-plan` |\n| Qwen 토큰 플랜(개인) | `QWEN_TOKEN_PLAN_API_KEY` | `qwen-token-plan-individual` |\n| Qwen 토큰 플랜(중국) | `QWEN_TOKEN_PLAN_CN_API_KEY` | `qwen-token-plan-cn` |\n| 샤오미 미모 | `XIAOMI_API_KEY` | `xiaomi` |\n| Xiaomi MiMo 토큰 플랜(중국) | `XIAOMI_TOKEN_PLAN_CN_API_KEY` | `xiaomi-token-plan-cn` |\n| Xiaomi MiMo 토큰 플랜(암스테르담) | `XIAOMI_TOKEN_PLAN_AMS_API_KEY` | `xiaomi-token-plan-ams` |\n| Xiaomi MiMo 토큰 플랜(싱가포르) | `XIAOMI_TOKEN_PLAN_SGP_API_KEY` | `xiaomi-token-plan-sgp` |\n\n환경 변수 및 `auth.json` 키에 대한 참조: [`packages/ai/src/env-api-keys.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/env-api-keys.ts)의 [`const envMap`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/env-api-keys.ts).\n\n#### 인증 파일\n\n`~/.pi/agent/auth.json`에 자격 증명을 저장합니다.\n\n```json\n{\n  \"anthropic\": { \"type\": \"api_key\", \"key\": \"sk-ant-...\" },\n  \"ant-ling\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"openai\": { \"type\": \"api_key\", \"key\": \"sk-...\" },\n  \"deepseek\": { \"type\": \"api_key\", \"key\": \"sk-...\" },\n  \"nvidia\": { \"type\": \"api_key\", \"key\": \"nvapi-...\" },\n  \"google\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"opencode\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"opencode-go\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"together\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"qwen-token-plan\":  { \"type\": \"api_key\", \"key\": \"sk-sp-...\" },\n  \"qwen-token-plan-individual\": { \"type\": \"api_key\", \"key\": \"sk-sp-...\" },\n  \"qwen-token-plan-cn\": { \"type\": \"api_key\", \"key\": \"sk-sp-...\" },\n  \"xiaomi\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"xiaomi-token-plan-cn\":  { \"type\": \"api_key\", \"key\": \"...\" },\n  \"xiaomi-token-plan-ams\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"xiaomi-token-plan-sgp\": { \"type\": \"api_key\", \"key\": \"...\" }\n}\n```\n\n`qwen-token-plan-individual`는 동일한 국제 엔드포인트를 사용하고 `QWEN_TOKEN_PLAN_API_KEY`는 다음과 같습니다.\n`qwen-token-plan`, 그러나 개별 구독에 대해 문서화된 모델로 선택기를 제한합니다. 기존\n공급자는 이전 버전과의 호환성을 위해 더 광범위한 카탈로그를 유지합니다. `auth.json`를 사용할 때\n귀하가 선택한 제공업체의 자격 증명 환경 변수는 두 국제 공급자가 공유합니다.\n\n파일은 `0600` 권한(사용자 읽기/쓰기 전용)으로 생성됩니다. 인증 파일 자격 증명은 환경 변수보다 우선합니다.\n\nAPI key 자격 증명에는 공급자 범위 환경 값도 포함될 수 있습니다. 이러한 값은 Cloudflare 계정 ID, Azure OpenAI 설정, Vertex 프로젝트/위치, Bedrock 설정, `PI_CACHE_RETENTION` 및 `HTTP_PROXY`/`HTTPS_PROXY`와 같은 자격 증명 키, 공급자/모델 헤더 및 공급자 구성을 확인할 때 프로세스 환경 변수 앞에 사용됩니다.\n\n```json\n{\n  \"cloudflare-ai-gateway\": {\n    \"type\": \"api_key\",\n    \"key\": \"$CLOUDFLARE_API_KEY\",\n    \"env\": {\n      \"CLOUDFLARE_API_KEY\": \"...\",\n      \"CLOUDFLARE_ACCOUNT_ID\": \"account-id\",\n      \"CLOUDFLARE_GATEWAY_ID\": \"gateway-id\"\n    }\n  }\n}\n```\n\npi가 프로젝트 셸 환경과 다른 공급자 설정을 사용해야 하는 경우 이를 사용합니다.\n\n### 주요 해결\n\n`key` 필드는 명령 실행, 환경 보간 및 리터럴을 지원합니다.\n\n- **셸 명령:** 시작 시 `\"!command\"` 전체 값을 명령으로 실행하고 stdout(프로세스 수명 동안 캐시됨)을 사용합니다.\n  ```json\n  { \"type\": \"api_key\", \"key\": \"!security find-generic-password -ws 'anthropic'\" }\n  { \"type\": \"api_key\", \"key\": \"!op read 'op://vault/item/credential'\" }\n  ```\n- **환경 보간:** `\"$ENV_VAR\"` 또는 `\"${ENV_VAR}\"`는 명명된 변수의 값을 사용합니다. 보간은 더 큰 리터럴 내에서 작동합니다.\n  ```json\n  { \"type\": \"api_key\", \"key\": \"$MY_ANTHROPIC_KEY\" }\n  { \"type\": \"api_key\", \"key\": \"${KEY_PREFIX}_${KEY_SUFFIX}\" }\n  ```\n  `$FOO_BAR`는 변수 `FOO_BAR`입니다. `BAR`가 리터럴 텍스트인 경우 `${FOO}_BAR`를 사용하세요. 환경 변수가 누락되면 값이 확인되지 않습니다.\n- **이스케이프:** `\"$\"`는 리터럴 `\"$\"`을 내보냅니다. `\"$!\"`는 명령 실행을 트리거하지 않고 리터럴 `\"!\"`을 내보냅니다.\n  ```json\n  { \"type\": \"api_key\", \"key\": \"$$literal-dollar-prefix\" }\n  { \"type\": \"api_key\", \"key\": \"$!literal-bang-prefix\" }\n  ```\n- **리터럴 값:** 직접 사용됩니다. `MY_API_KEY`와 같은 일반 대문자 문자열은 리터럴입니다. 환경 변수에는 `$MY_API_KEY`를 사용하세요.\n  ```json\n  { \"type\": \"api_key\", \"key\": \"sk-ant-...\" }\n  { \"type\": \"api_key\", \"key\": \"public\" }\n  ```\n\nOAuth 자격 증명은 `/login` 이후에도 여기에 저장되어 자동으로 관리됩니다.\n\n## 클라우드 Providers\n\n### Azure OpenAI\n\n```bash\nexport AZURE_OPENAI_API_KEY=...\nexport AZURE_OPENAI_BASE_URL=https://your-resource.ai.azure.com\n# also supported: https://your-resource.cognitiveservices.azure.com\n# also supported: https://your-resource.openai.azure.com\n# root endpoints are auto-normalized to /openai/v1\n# or use resource name instead of base URL\nexport AZURE_OPENAI_RESOURCE_NAME=your-resource\n\n# Optional\nexport AZURE_OPENAI_API_VERSION=2024-02-01\nexport AZURE_OPENAI_DEPLOYMENT_NAME_MAP=gpt-4=my-gpt4,gpt-4o=my-gpt4o\n```\n\n### 아마존 기반암\n\n`/login amazon-bedrock`를 사용하여 Bedrock API key을 저장하거나 아래의 주변 AWS 자격 증명 소스 중 하나를 구성합니다.\n\n```bash\n# Option 1: AWS Profile\nexport AWS_PROFILE=your-profile\n\n# Option 2: IAM Keys\nexport AWS_ACCESS_KEY_ID=AKIA...\nexport AWS_SECRET_ACCESS_KEY=...\n\n# Option 3: Bearer Token\nexport AWS_BEARER_TOKEN_BEDROCK=...\n\n# Optional region (defaults to us-east-1)\nexport AWS_REGION=us-west-2\n```\n\nECS 작업 역할(`AWS_CONTAINER_CREDENTIALS_*`) 및 IRSA(`AWS_WEB_IDENTITY_TOKEN_FILE`)도 지원합니다.\n\n```bash\npi --provider amazon-bedrock --model us.anthropic.claude-sonnet-4-20250514-v1:0\n```\n\nID에 인식 가능한 모델 이름이 포함된 Claude 모델(기본 모델 및 시스템 정의 추론 프로필)에 대해서는 프롬프트 캐싱이 자동으로 활성화됩니다. 애플리케이션 추론 프로필(ARN에 모델 이름이 포함되지 않음)의 경우 `AWS_BEDROCK_FORCE_CACHE=1`를 설정하여 캐시 포인트를 활성화합니다.\n\n```bash\nexport AWS_BEDROCK_FORCE_CACHE=1\npi --provider amazon-bedrock --model arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/abc123\n```\n\nBedrock API 프록시에 연결하는 경우 다음 환경 변수를 사용할 수 있습니다.\n\n```bash\n# Set the URL for the Bedrock proxy (standard AWS SDK env var)\nexport AWS_ENDPOINT_URL_BEDROCK_RUNTIME=https://my.corp.proxy/bedrock\n\n# Set if your proxy does not require authentication\nexport AWS_BEDROCK_SKIP_AUTH=1\n\n# Set if your proxy only supports HTTP/1.1\nexport AWS_BEDROCK_FORCE_HTTP1=1\n```\n\n### Cloudflare AI 게이트웨이\n\n`CLOUDFLARE_API_KEY`는 `/login`를 통해 설정할 수 있습니다. 계정 ID와 게이트웨이 슬러그는 환경 변수로 설정하거나 `auth.json`에 있는 API key 자격 증명의 `env` 개체에서 설정할 수 있습니다.\n\n```bash\nexport CLOUDFLARE_API_KEY=...           # or use /login\nexport CLOUDFLARE_ACCOUNT_ID=...\nexport CLOUDFLARE_GATEWAY_ID=...        # create at dash.cloudflare.com → AI → AI Gateway\npi --provider cloudflare-ai-gateway --model \"claude-sonnet-4-5\"\n```\n\nCloudflare AI Gateway를 통해 OpenAI, Anthropic 및 Workers AI로 라우팅합니다. Workers AI는 통합 API(`/compat`) 및 접두사가 붙은 모델 ID(`workers-ai/@cf/...`)를 사용합니다. OpenAI는 `gpt-5.1`와 같은 기본 OpenAI 모델 ID와 함께 OpenAI 통과 경로(`/openai`)를 사용합니다. Anthropic은 `claude-sonnet-4-5`와 같은 기본 Anthropic 모델 ID와 함께 Anthropic 통과 경로(`/anthropic`)를 사용합니다.\n\nAI Gateway 인증에서는 `CLOUDFLARE_API_KEY`를 `cf-aig-authorization`로 사용합니다. 업스트림 인증은 다음 중 하나일 수 있습니다.\n\n| 방법 | 인증 요청 | 업스트림 인증 |\n|------|--------------|---------------|\n| 노동자 AI | Cloudflare 토큰만 | Cloudflare 기반 |\n| 통합 결제 | Cloudflare 토큰만 | Cloudflare는 업스트림 인증을 처리하고 크레딧을 차감합니다. |\n| BYOK 저장됨 | Cloudflare 토큰만 | Cloudflare는 AI Gateway 대시보드에 저장된 공급자 키를 삽입합니다. |\n| 인라인 BYOK | Cloudflare 토큰과 업스트림 `Authorization` 헤더 | 요청은 업스트림 공급자 키를 제공합니다. |\n\n일반적인 Pi 사용의 경우 통합 청구 또는 저장된 BYOK를 선호합니다. 인라인 BYOK를 사용하려면 `models.json` 공급자/모델 재정의 등을 통해 Cloudflare AI Gateway 공급자에 대한 추가 업스트림 `Authorization` 헤더를 구성해야 합니다.\n\n### Cloudflare 작업자 AI\n\n`CLOUDFLARE_API_KEY`는 `/login`를 통해 설정할 수 있습니다. `CLOUDFLARE_ACCOUNT_ID`는 환경 변수로 설정하거나 `auth.json`에 있는 API key 자격 증명의 `env` 개체에 설정할 수 있습니다.\n\n```bash\nexport CLOUDFLARE_API_KEY=...           # or use /login\nexport CLOUDFLARE_ACCOUNT_ID=...\npi --provider cloudflare-workers-ai --model \"@cf/moonshotai/kimi-k2.6\"\n```\n\nPi는 [prefix caching](https://developers.cloudflare.com/workers-ai/features/prompt-caching/) 할인을 위해 `x-session-affinity`를 자동으로 설정합니다.\n\n### 구글 버텍스 AI\n\n애플리케이션 기본 자격 증명 사용:\n\n```bash\ngcloud auth application-default login\nexport GOOGLE_CLOUD_PROJECT=your-project\nexport GOOGLE_CLOUD_LOCATION=us-central1\n```\n\n또는 `GOOGLE_APPLICATION_CREDENTIALS`를 서비스 계정 키 파일로 설정하세요.\n\n## llama.cpp\n\nPi는 llama.cpp 라우터 서버를 지원합니다. `/login llama.cpp`로 구성하고, `/llama`로 로드된 모델을 관리하고, `/model`로 로드된 모델을 선택하세요.\n\n서버 설정, 모델 디렉터리 레이아웃, 환경 변수 및 명령 사용법은 [llama.cpp](llama-cpp.md)를 참조하세요.\n\n## 맞춤 Providers\n\n**models.json을 통해:** Ollama, LM Studio, vLLM 또는 지원되는 API(OpenAI Completions, OpenAI Responses, Anthropic Messages, Google Generative AI)을 사용하는 모든 공급자를 추가하세요. [models.md](models.md)를 참조하세요.\n\n**확장 프로그램을 통해:** 맞춤 API 구현 또는 OAuth 흐름이 필요한 공급자의 경우 확장 프로그램을 만듭니다. [custom-provider.md](custom-provider.md) 및 [examples/extensions/custom-provider-gitlab-duo](../examples/extensions/custom-provider-gitlab-duo/)를 참조하세요.\n\n## 해결 순서\n\n공급자에 대한 자격 증명을 확인할 때:\n\n1. CLI `--api-key` 깃발\n2. `auth.json` 항목(API key 또는 OAuth 토큰)\n3. 환경변수\n4. `models.json`의 맞춤 공급자 키","sourceFile":"providers.md"},"quickstart":{"title":"빠른 시작","markdown":"이 페이지에서는 설치부터 유용한 첫 번째 Pi 세션까지 안내합니다.\n\n## 설치하다\n\nPi는 npm 패키지로 배포됩니다.\n\n```bash\nnpm install -g --ignore-scripts @earendil-works/pi-coding-agent\n```\n\n`--ignore-scripts` 설치 중에 종속성 수명 주기 스크립트를 비활성화합니다. Pi 일반 npm 설치에는 설치 스크립트가 필요하지 않습니다.\n\n### 제거\n\npi를 설치한 패키지 관리자를 사용하십시오. 컬 설치 프로그램은 npm를 전역적으로 사용하므로 컬 및 npm 설치는 npm를 사용하여 제거됩니다.\n\n```bash\n# curl installer or npm install -g\nnpm uninstall -g @earendil-works/pi-coding-agent\n\n# pnpm\npnpm remove -g @earendil-works/pi-coding-agent\n\n# Yarn\nyarn global remove @earendil-works/pi-coding-agent\n\n# Bun\nbun uninstall -g @earendil-works/pi-coding-agent\n```\n\npi를 제거하면 설정, 자격 증명, 세션 및 설치된 pi 패키지가 `~/.pi/agent/`에 남습니다.\n\n그런 다음 작업하려는 프로젝트 디렉터리에서 pi를 시작합니다.\n\n```bash\ncd /path/to/project\npi\n```\n\n## 인증하다\n\nPi는 환경 변수나 인증 파일을 통해 subscription providers부터 `/login`까지 또는 API 키 공급자를 사용할 수 있습니다.\n\n### 옵션 1: 구독 로그인\n\npi를 시작하고 다음을 실행합니다.\n\n```text\n/login\n```\n\n그런 다음 공급자를 선택하십시오. 내장된 구독 로그인에는 Claude Pro/Max, ChatGPT Plus/Pro(Codex) 및 GitHub Copilot이 포함됩니다.\n\n### 옵션 2: API key\n\npi를 시작하기 전에 API key를 설정하세요.\n\n```bash\nexport ANTHROPIC_API_KEY=sk-ant-...\npi\n```\n\n`/login`를 실행하고 API 키 공급자를 선택하여 `~/.pi/agent/auth.json`에 키를 저장할 수도 있습니다.\n\n지원되는 모든 공급자, 환경 변수 및 클라우드 공급자 설정은 [Providers](providers.md)를 참조하세요.\n\n## 첫 번째 세션\n\npi가 시작되면 요청을 입력하고 Enter를 누릅니다.\n\n```text\nSummarize this repository and tell me how to run its checks.\n```\n\n기본적으로 pi는 모델에 네 가지 도구를 제공합니다.\n\n- `read` - 파일 읽기\n- `write` - 파일 생성 또는 덮어쓰기\n- `edit` - 패치 파일\n- `bash` - 쉘 명령 실행\n\n추가로 내장된 읽기 전용 도구(`grep`, `find`, `ls`)는 도구 옵션을 통해 사용할 수 있습니다. Pi 현재 작업 디렉터리에서 실행되며 거기에서 파일을 수정할 수 있습니다. 쉬운 롤백을 원한다면 git 또는 다른 체크포인트 워크플로를 사용하세요.\n\n## Pi 프로젝트 지침 제공\n\nPi는 시작 시 context files를 로드합니다. 프로젝트에서 작업하는 방법을 알려주려면 `AGENTS.md` 파일을 추가하세요.\n\n```markdown\n# Project Instructions\n\n- Run `npm run check` after code changes.\n- Do not run production migrations locally.\n- Keep responses concise.\n```\n\nPi 로드:\n\n- `~/.pi/agent/AGENTS.md` 글로벌 지침\n- `AGENTS.md` 또는 `CLAUDE.md` 상위 디렉토리 및 현재 디렉토리에서\n\n디렉토리에 `AGENTS.override.md`가 포함되어 있으면 Pi는 해당 디렉토리에서 `AGENTS.md` 또는 `CLAUDE.md` 대신 해당 디렉토리를 로드합니다.\n\npi를 다시 시작하거나 context files를 변경한 후 `/reload`를 실행하세요.\n\n## 일반적으로 시도해 볼 사항\n\n### 참조 파일\n\n편집기에서 `@`를 입력하여 파일을 유사 검색하거나 명령줄에 파일을 전달하세요.\n\n```bash\npi @README.md \"Summarize this\"\npi @src/app.ts @src/app.test.ts \"Review these together\"\n```\n\n이미지나 텍스트는 Ctrl+V(Windows에서는 Alt+V)를 사용하여 붙여넣을 수 있습니다. 이미지를 지원되는 터미널로 드래그할 수도 있습니다.\n\n### 쉘 명령 실행\n\n대화형 모드에서:\n\n```text\n!npm run lint\n```\n\n명령 출력이 모델로 전송됩니다. 모델 컨텍스트에 출력을 추가하지 않고 명령을 실행하려면 `!!command`를 사용하세요.\n\n### 모델 전환\n\n모델을 선택하려면 `/model` 또는 Ctrl+L을 사용하세요. 사고 수준을 순환하려면 Shift+Tab을 사용하세요. 범위가 지정된 모델을 순환하려면 Ctrl+P / Shift+Ctrl+P를 사용하세요.\n\n### 나중에 계속\n\n세션은 자동으로 저장됩니다.\n\n```bash\npi -c                  # Continue most recent session\npi -r                  # Browse previous sessions\npi --name \"my task\"    # Set session display name at startup\npi --session <path|id> # Open a specific session\n```\n\npi 내부에서 `/resume`, `/new`, `/tree`, `/fork` 및 `/clone`를 사용하여 세션을 관리합니다.\n\n### 비대화형 모드\n\n일회성 프롬프트의 경우:\n\n```bash\npi -p \"Summarize this codebase\"\ncat README.md | pi -p \"Summarize this text\"\npi -p @screenshot.png \"What's in this image?\"\n```\n\nJSON 이벤트 출력에는 `--mode json`를 사용하고, 프로세스 통합에는 `--mode rpc`를 사용하세요.\n\n## 다음 단계\n\n- [Using Pi](usage.md) - 대화형 모드, slash commands, 세션, context files 및 CLI 참조.\n- [Providers](providers.md) - 인증 및 모델 설정.\n- [Settings](settings.md) - 전역 및 프로젝트 구성.\n- [Keybindings](keybindings.md) - 바로가기 및 맞춤설정.\n- [Pi Packages](packages.md) - 공유 확장, 기술, 프롬프트 및 테마를 설치합니다.\n\n플랫폼 참고 사항: [Windows](windows.md), [Termux](termux.md), [tmux](tmux.md), [Terminal setup](terminal-setup.md), [Shell aliases](shell-aliases.md).","sourceFile":"quickstart.md"},"rpc":{"title":"RPC 모드","markdown":"RPC 모드는 stdin/stdout에 대한 JSON 프로토콜을 통해 코딩 에이전트의 헤드리스 작업을 활성화합니다. 이는 다른 애플리케이션, IDE 또는 사용자 정의 UI에 에이전트를 포함하는 데 유용합니다.\n\n**Node.js/TypeScript 사용자를 위한 참고사항**: Node.js 애플리케이션을 구축하는 경우 하위 프로세스를 생성하는 대신 `@earendil-works/pi-coding-agent`에서 직접 `AgentSession`를 사용하는 것이 좋습니다. API에 대해서는 [`src/core/agent-session.ts`](../src/core/agent-session.ts)를 참조하세요. 하위 프로세스 기반 TypeScript 클라이언트의 경우 [`src/modes/rpc/rpc-client.ts`](../src/modes/rpc/rpc-client.ts)를 참조하세요.\n\n## RPC 모드 시작 중\n\n```bash\npi --mode rpc [options]\n```\n\n일반적인 옵션:\n- `--provider <name>`: LLM 제공업체 설정(anthropic, openai, google 등)\n- `--model <pattern>`: 모델 패턴 또는 ID(`provider/id` 지원 및 선택 사항 `:<thinking>`)\n- `--name <name>` / `-n <name>`: 시작 시 세션 표시 이름 설정\n- `--no-session`: 세션 지속성 비활성화\n- `--session-dir <path>`: 사용자 정의 세션 저장 디렉터리\n\n## 프로토콜 개요\n\n- **명령**: JSON 객체가 stdin로 전송됨(한 줄에 하나씩)\n- **응답**: JSON 개체(명령 성공/실패를 나타내는 `type: \"response\"` 포함)\n- **이벤트**: 에이전트 이벤트가 JSON 라인으로 stdout로 스트리밍됩니다.\n\n모든 명령은 요청/응답 상관 관계에 대한 선택적 `id` 필드를 지원합니다. 제공된 경우 해당 응답에는 동일한 `id`이 포함됩니다. `bash_execution_update` 이벤트에는 원래 `bash` 명령의 `id`도 포함됩니다.\n\n### 프레이밍\n\nRPC 모드는 LF(`\\n`)를 유일한 레코드 구분 기호로 사용하는 엄격한 JSONL 의미 체계를 사용합니다.\n\n이는 고객에게 중요합니다.\n- `\\n`에서만 기록 분할\n- 후행 `\\r`을 제거하여 선택적 `\\r\\n` 입력을 허용합니다.\n- 유니코드 구분 기호를 줄 바꿈으로 처리하는 일반 줄 판독기를 사용하지 마세요.\n\n특히 노드 `readline`는 JSON 문자열 내에서 유효한 `U+2028` 및 `U+2029`에서도 분할되기 때문에 RPC 모드에 대한 프로토콜을 준수하지 않습니다.\n\n## 명령\n\n### 격려\n\n#### 즉각적인\n\n사용자 프롬프트를 에이전트에게 보냅니다. 프롬프트가 수락되거나 대기열에 추가되거나 처리된 후에 명령 응답이 내보내집니다. 이벤트는 승인 후에도 비동기식으로 계속 스트리밍됩니다.\n\n```json\n{\"id\": \"req-1\", \"type\": \"prompt\", \"message\": \"Hello, world!\"}\n```\n\n이미지 포함:\n```json\n{\"type\": \"prompt\", \"message\": \"What's in this image?\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\n**스트리밍 중**: 에이전트가 이미 스트리밍 중인 경우 메시지를 대기열에 추가하려면 `streamingBehavior`를 지정해야 합니다.\n\n```json\n{\"type\": \"prompt\", \"message\": \"New instruction\", \"streamingBehavior\": \"steer\"}\n```\n\n- `\"steer\"`: 에이전트가 실행되는 동안 메시지를 대기열에 넣습니다. 현재 보조 차례가 도구 호출 실행을 마친 후 다음 LLM 호출 전에 전달됩니다.\n- `\"followUp\"`: 에이전트가 완료될 때까지 기다립니다. 에이전트가 중지된 경우에만 메시지가 전달됩니다.\n\n에이전트가 스트리밍 중이고 `streamingBehavior`가 지정되지 않은 경우 명령은 오류를 반환합니다.\n\n**확장 명령**: 메시지가 확장 명령(예: `/mycommand`)인 경우 스트리밍 중에도 즉시 실행됩니다. 확장 명령은 `pi.sendMessage()`을 통해 자체 LLM 상호 작용을 관리합니다.\n\n**입력 확장**: 스킬 명령(`/skill:name`) 및 prompt templates(`/template`)이 전송/대기 전에 확장됩니다.\n\n응답:\n```json\n{\"id\": \"req-1\", \"type\": \"response\", \"command\": \"prompt\", \"success\": true}\n```\n\n`success: true`는 프롬프트가 수락, 대기 또는 즉시 처리되었음을 의미합니다. `success: false` 메시지가 수락되기 전에 거부되었음을 의미합니다. 수락 후 실패는 동일한 요청 ID에 대해 두 번째 `response`가 아닌 일반 이벤트 및 메시지 스트림을 통해 보고됩니다.\n\n`images` 필드는 선택사항입니다. 각 이미지는 `ImageContent` 형식(`{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}`)을 사용합니다.\n\n#### 수송아지\n\n에이전트가 실행되는 동안 조정 메시지를 대기열에 추가합니다. 현재 보조 차례가 도구 호출 실행을 마친 후 다음 LLM 호출 전에 전달됩니다. 스킬 명령어와 prompt templates가 확장됩니다. 확장 명령은 허용되지 않습니다(대신 `prompt` 사용).\n\n```json\n{\"type\": \"steer\", \"message\": \"Stop and do this instead\"}\n```\n\n이미지 포함:\n```json\n{\"type\": \"steer\", \"message\": \"Look at this instead\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\n`images` 필드는 선택사항입니다. 각 이미지는 `ImageContent` 형식(`prompt`과 동일)을 사용합니다.\n\n응답:\n```json\n{\"type\": \"response\", \"command\": \"steer\", \"success\": true}\n```\n\n조정 메시지 처리 방법을 제어하려면 [set_steering_mode](#set_steering_mode)를 참조하세요.\n\n#### 후속 조치\n\n에이전트가 완료된 후 처리할 후속 메시지를 대기열에 추가합니다. 상담원에게 더 이상 도구 호출이나 조정 메시지가 없는 경우에만 전달됩니다. 스킬 명령어와 prompt templates가 확장됩니다. 확장 명령은 허용되지 않습니다(대신 `prompt` 사용).\n\n```json\n{\"type\": \"follow_up\", \"message\": \"After you're done, also do this\"}\n```\n\n이미지 포함:\n```json\n{\"type\": \"follow_up\", \"message\": \"Also check this image\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\n`images` 필드는 선택사항입니다. 각 이미지는 `ImageContent` 형식(`prompt`과 동일)을 사용합니다.\n\n응답:\n```json\n{\"type\": \"response\", \"command\": \"follow_up\", \"success\": true}\n```\n\n후속 메시지 처리 방법을 제어하려면 [set_follow_up_mode](#set_follow_up_mode)를 참조하세요.\n\n#### 중단하다\n\n현재 에이전트 작업을 중단합니다.\n\n```json\n{\"type\": \"abort\"}\n```\n\n응답:\n```json\n{\"type\": \"response\", \"command\": \"abort\", \"success\": true}\n```\n\n#### new_session\n\n새로운 세션을 시작하세요. `session_before_switch` 확장 이벤트 핸들러로 취소할 수 있습니다.\n\n```json\n{\"type\": \"new_session\"}\n```\n\n선택적 상위 세션 추적 사용:\n```json\n{\"type\": \"new_session\", \"parentSession\": \"/path/to/parent-session.jsonl\"}\n```\n\n응답:\n```json\n{\"type\": \"response\", \"command\": \"new_session\", \"success\": true, \"data\": {\"cancelled\": false}}\n```\n\n연장이 취소된 경우:\n```json\n{\"type\": \"response\", \"command\": \"new_session\", \"success\": true, \"data\": {\"cancelled\": true}}\n```\n\n### 상태\n\n#### get_state\n\n현재 세션 상태를 가져옵니다.\n\n```json\n{\"type\": \"get_state\"}\n```\n\n응답:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_state\",\n  \"success\": true,\n  \"data\": {\n    \"model\": {...},\n    \"thinkingLevel\": \"medium\",\n    \"isStreaming\": false,\n    \"isCompacting\": false,\n    \"steeringMode\": \"all\",\n    \"followUpMode\": \"one-at-a-time\",\n    \"sessionFile\": \"/path/to/session.jsonl\",\n    \"sessionId\": \"abc123\",\n    \"sessionName\": \"my-feature-work\",\n    \"autoCompactionEnabled\": true,\n    \"messageCount\": 5,\n    \"pendingMessageCount\": 0\n  }\n}\n```\n\n`model` 필드는 전체 [Model](#model) 개체 또는 `null`입니다. `sessionName` 필드는 `set_session_name`를 통해 설정된 표시 이름이며, 설정되지 않은 경우 생략됩니다.\n\n#### get_messages\n\n대화의 모든 메시지를 가져옵니다.\n\n```json\n{\"type\": \"get_messages\"}\n```\n\n응답:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_messages\",\n  \"success\": true,\n  \"data\": {\"messages\": [...]}\n}\n```\n\n메시지는 `AgentMessage` 개체입니다([Message Types](#message-types) 참조).\n\n### 모델\n\n#### 세트_모델\n\n특정 모델로 전환하십시오.\n\n```json\n{\"type\": \"set_model\", \"provider\": \"anthropic\", \"modelId\": \"claude-sonnet-4-20250514\"}\n```\n\n응답에는 전체 [Model](#model) 개체가 포함됩니다.\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"set_model\",\n  \"success\": true,\n  \"data\": {...}\n}\n```\n\n#### 사이클_모델\n\n사용 가능한 다음 모델로 순환합니다. 모델이 하나만 사용 가능한 경우 `null` 데이터를 반환합니다.\n\n```json\n{\"type\": \"cycle_model\"}\n```\n\n응답:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"cycle_model\",\n  \"success\": true,\n  \"data\": {\n    \"model\": {...},\n    \"thinkingLevel\": \"medium\",\n    \"isScoped\": false\n  }\n}\n```\n\n`model` 필드는 전체 [Model](#model) 개체입니다.\n\n#### get_available_models\n\n구성된 모델을 모두 나열합니다.\n\n```json\n{\"type\": \"get_available_models\"}\n```\n\n응답에는 전체 [Model](#model) 객체 배열이 포함됩니다.\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_available_models\",\n  \"success\": true,\n  \"data\": {\n    \"models\": [...]\n  }\n}\n```\n\n### 생각\n\n#### set_thinking_level\n\n이를 지원하는 모델에 대한 추론/사고 수준을 설정합니다.\n\n```json\n{\"type\": \"set_thinking_level\", \"level\": \"high\"}\n```\n\n레벨: `\"off\"`, `\"minimal\"`, `\"low\"`, `\"medium\"`, `\"high\"`, `\"xhigh\"`, `\"max\"`\n\n`\"xhigh\"` 및 `\"max\"`는 선택한 모델에서 지원하는 경우에만 노출됩니다. GPT-5.6을 포함한 일부 모델은 둘 다 노출합니다.\n\n응답:\n```json\n{\"type\": \"response\", \"command\": \"set_thinking_level\", \"success\": true}\n```\n\n#### 주기_사고_수준\n\n사용 가능한 사고 수준을 순환합니다. 모델이 사고를 지원하지 않는 경우 `null` 데이터를 반환합니다.\n\n```json\n{\"type\": \"cycle_thinking_level\"}\n```\n\n응답:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"cycle_thinking_level\",\n  \"success\": true,\n  \"data\": {\"level\": \"high\"}\n}\n```\n\n#### get_available_thinking_levels\n\n현재 모델이 지원하는 사고 수준을 나열하십시오. 추론 지원이 없는 모델의 경우 `[\"off\"]`를 반환합니다.\n\n```json\n{\"type\": \"get_available_thinking_levels\"}\n```\n\n응답:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_available_thinking_levels\",\n  \"success\": true,\n  \"data\": {\n    \"levels\": [\"off\", \"minimal\", \"low\", \"medium\", \"high\"]\n  }\n}\n```\n\n### 대기열 모드\n\n#### set_steering_mode\n\n조정 메시지(`steer`부터)가 전달되는 방식을 제어합니다.\n\n```json\n{\"type\": \"set_steering_mode\", \"mode\": \"one-at-a-time\"}\n```\n\n모드:\n- `\"all\"`: 현재 보조 턴이 도구 호출 실행을 마친 후 모든 조향 메시지를 전달합니다.\n- `\"one-at-a-time\"`: 보조 회전 완료 시 하나의 조향 메시지 전달(기본값)\n\n응답:\n```json\n{\"type\": \"response\", \"command\": \"set_steering_mode\", \"success\": true}\n```\n\n#### set_follow_up_mode\n\n후속 메시지(`follow_up`에서)가 전달되는 방식을 제어합니다.\n\n```json\n{\"type\": \"set_follow_up_mode\", \"mode\": \"one-at-a-time\"}\n```\n\n모드:\n- `\"all\"`: 에이전트가 완료되면 모든 후속 메시지 전달\n- `\"one-at-a-time\"`: 에이전트 완료당 하나의 후속 메시지 전달(기본값)\n\n응답:\n```json\n{\"type\": \"response\", \"command\": \"set_follow_up_mode\", \"success\": true}\n```\n\n### 압축\n\n#### 콤팩트\n\n토큰 사용량을 줄이기 위해 대화 컨텍스트를 수동으로 압축합니다.\n\n```json\n{\"type\": \"compact\"}\n```\n\n맞춤 지침 사용:\n```json\n{\"type\": \"compact\", \"customInstructions\": \"Focus on code changes\"}\n```\n\n응답:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"compact\",\n  \"success\": true,\n  \"data\": {\n    \"summary\": \"Summary of conversation...\",\n    \"firstKeptEntryId\": \"abc123\",\n    \"tokensBefore\": 150000,\n    \"estimatedTokensAfter\": 32000,\n    \"usage\": {\n      \"input\": 32000,\n      \"output\": 1200,\n      \"cacheRead\": 0,\n      \"cacheWrite\": 0,\n      \"totalTokens\": 33200,\n      \"cost\": {\"input\": 0.01, \"output\": 0.02, \"cacheRead\": 0, \"cacheWrite\": 0, \"total\": 0.03}\n    },\n    \"details\": {}\n  }\n}\n```\n\n`estimatedTokensAfter`은 제공자에 따른 정확한 토큰 수가 아니라 압축 직후 다시 작성된 메시지 컨텍스트에 대한 경험적 추정치입니다. `usage` 요약을 생성한 LLM 호출을 보고하며 사용자 정의 압축 처리기에 의해 생략될 수 있습니다.\n\n#### set_auto_comaction\n\n컨텍스트가 거의 가득 찼을 때 자동 압축을 활성화하거나 비활성화합니다.\n\n```json\n{\"type\": \"set_auto_compaction\", \"enabled\": true}\n```\n\n응답:\n```json\n{\"type\": \"response\", \"command\": \"set_auto_compaction\", \"success\": true}\n```\n\n### 다시 해 보다\n\n#### set_auto_retry\n\n일시적인 오류(오버로드, 속도 제한, 5xx)에 대한 자동 재시도를 활성화하거나 비활성화합니다.\n\n```json\n{\"type\": \"set_auto_retry\", \"enabled\": true}\n```\n\n응답:\n```json\n{\"type\": \"response\", \"command\": \"set_auto_retry\", \"success\": true}\n```\n\n#### 중단_재시도\n\n진행 중인 재시도를 중단합니다(지연을 취소하고 재시도를 중지).\n\n```json\n{\"type\": \"abort_retry\"}\n```\n\n응답:\n```json\n{\"type\": \"response\", \"command\": \"abort_retry\", \"success\": true}\n```\n\n### 세게 때리다\n\n#### bash\n\n셸 명령을 실행하고 대화 컨텍스트에 출력을 추가합니다. 명령이 실행되는 동안 출력은 `bash_execution_update` 이벤트로 스트리밍됩니다. 응답에는 최종 결과가 포함됩니다.\n\n```json\n{\"id\": \"req-1\", \"type\": \"bash\", \"command\": \"ls -la\"}\n```\n\n스트리밍된 `bash_execution_update` 이벤트를 이 명령과 연결하려면 `id`를 포함하세요.\n\n응답:\n```json\n{\n  \"id\": \"req-1\",\n  \"type\": \"response\",\n  \"command\": \"bash\",\n  \"success\": true,\n  \"data\": {\n    \"output\": \"total 48\\ndrwxr-xr-x ...\",\n    \"exitCode\": 0,\n    \"cancelled\": false,\n    \"truncated\": false\n  }\n}\n```\n\n출력이 잘린 경우 `fullOutputPath`를 포함합니다.\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"bash\",\n  \"success\": true,\n  \"data\": {\n    \"output\": \"truncated output...\",\n    \"exitCode\": 0,\n    \"cancelled\": false,\n    \"truncated\": true,\n    \"fullOutputPath\": \"/tmp/pi-bash-abc123.log\"\n  }\n}\n```\n\n**bash 결과가 LLM에 도달하는 방법:**\n\n`bash` 명령은 즉시 실행되고 `BashResult`을 반환합니다. 내부적으로는 `BashExecutionMessage`가 생성되어 에이전트의 메시지 상태에 저장됩니다.\n\n다음 `prompt` 명령이 전송되면 모든 메시지(`BashExecutionMessage` 포함)가 LLM으로 전송되기 전에 변환됩니다. `BashExecutionMessage`는 다음 형식으로 `UserMessage`로 변환됩니다.\n\n````\nRan `ls -la`\n```\n총 48개\ndrwxr-xr-x...\n```\n````\n\n이는 다음을 의미합니다.\n1. Bash 출력은 즉시 포함되지 않고 **다음 프롬프트**의 LLM 컨텍스트에 포함됩니다.\n2. 프롬프트가 표시되기 전에 여러 bash 명령을 실행할 수 있습니다. 모든 출력이 포함됩니다\n\n#### 중단_bash\n\n실행 중인 bash 명령을 중단합니다.\n\n```json\n{\"type\": \"abort_bash\"}\n```\n\n응답:\n```json\n{\"type\": \"response\", \"command\": \"abort_bash\", \"success\": true}\n```\n\n### 세션\n\n#### get_session_stats\n\n토큰 사용량, 비용 통계 및 현재 컨텍스트 창 사용량을 확인하세요.\n\n```json\n{\"type\": \"get_session_stats\"}\n```\n\n응답:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_session_stats\",\n  \"success\": true,\n  \"data\": {\n    \"sessionFile\": \"/path/to/session.jsonl\",\n    \"sessionId\": \"abc123\",\n    \"userMessages\": 5,\n    \"assistantMessages\": 5,\n    \"toolCalls\": 12,\n    \"toolResults\": 12,\n    \"totalMessages\": 22,\n    \"tokens\": {\n      \"input\": 50000,\n      \"output\": 10000,\n      \"cacheRead\": 40000,\n      \"cacheWrite\": 5000,\n      \"total\": 105000\n    },\n    \"cost\": 0.45,\n    \"contextUsage\": {\n      \"tokens\": 60000,\n      \"contextWindow\": 200000,\n      \"percent\": 30\n    }\n  }\n}\n```\n\n`tokens` 및 `cost`에는 보조 메시지, 도구에서 보고된 사용량, 전체 세션에 걸친 압축/분기 요약 생성이 포함됩니다. `contextUsage` 압축 및 바닥글 표시에 사용되는 실제 현재 컨텍스트 창 추정값이 포함되어 있습니다.\n\n`contextUsage`는 모델이나 컨텍스트 창을 사용할 수 없는 경우 생략됩니다. `contextUsage.tokens` 및 `contextUsage.percent`는 새로운 압축 후 보조 응답이 유효한 사용 데이터를 제공할 때까지 압축 직후 `null`입니다.\n\n#### 내보내기_html\n\n세션을 HTML 파일로 내보냅니다.\n\n```json\n{\"type\": \"export_html\"}\n```\n\n사용자 정의 경로 사용:\n```json\n{\"type\": \"export_html\", \"outputPath\": \"/tmp/session.html\"}\n```\n\n응답:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"export_html\",\n  \"success\": true,\n  \"data\": {\"path\": \"/tmp/session.html\"}\n}\n```\n\n#### 스위치 세션\n\n다른 세션 파일을 로드합니다. `session_before_switch` 확장 이벤트 핸들러로 취소할 수 있습니다.\n\n```json\n{\"type\": \"switch_session\", \"sessionPath\": \"/path/to/session.jsonl\"}\n```\n\n응답:\n```json\n{\"type\": \"response\", \"command\": \"switch_session\", \"success\": true, \"data\": {\"cancelled\": false}}\n```\n\n확장 프로그램이 스위치를 취소한 경우:\n```json\n{\"type\": \"response\", \"command\": \"switch_session\", \"success\": true, \"data\": {\"cancelled\": true}}\n```\n\n#### 포크\n\n활성 분기의 이전 사용자 메시지에서 새 포크를 만듭니다. `session_before_fork` 확장 이벤트 핸들러로 취소할 수 있습니다. 포크되는 메시지의 텍스트를 반환합니다.\n\n```json\n{\"type\": \"fork\", \"entryId\": \"abc123\"}\n```\n\n응답:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"fork\",\n  \"success\": true,\n  \"data\": {\"text\": \"The original prompt text...\", \"cancelled\": false}\n}\n```\n\n확장 프로그램이 포크를 취소한 경우:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"fork\",\n  \"success\": true,\n  \"data\": {\"text\": \"The original prompt text...\", \"cancelled\": true}\n}\n```\n\n#### 클론\n\n현재 활성 분기를 현재 위치의 새 세션에 복제합니다. `session_before_fork` 확장 이벤트 핸들러로 취소할 수 있습니다.\n\n```json\n{\"type\": \"clone\"}\n```\n\n응답:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"clone\",\n  \"success\": true,\n  \"data\": {\"cancelled\": false}\n}\n```\n\n확장 프로그램이 복제를 취소한 경우:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"clone\",\n  \"success\": true,\n  \"data\": {\"cancelled\": true}\n}\n```\n\n#### get_fork_messages\n\n포크할 수 있는 사용자 메시지를 가져옵니다.\n\n```json\n{\"type\": \"get_fork_messages\"}\n```\n\n응답:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_fork_messages\",\n  \"success\": true,\n  \"data\": {\n    \"messages\": [\n      {\"entryId\": \"abc123\", \"text\": \"First prompt...\"},\n      {\"entryId\": \"def456\", \"text\": \"Second prompt...\"}\n    ]\n  }\n}\n```\n\n#### get_entries\n\n모든 세션 항목을 추가 순서로 가져옵니다(세션 헤더 제외). 세션은 안정적인 ID가 있는 항목의 추가 전용 트리이므로 항목 ID는 내구성 있는 커서로 작동합니다. 클라이언트를 다시 시작해도 해당 항목 이후의 항목만 가져오려면 `since`로 표시된 마지막 항목 ID를 전달합니다. `get_messages`와 달리 여기에는 압축 전 기록과 버려진 분기가 포함됩니다.\n\n```json\n{\"type\": \"get_entries\"}\n```\n\n커서 사용:\n```json\n{\"type\": \"get_entries\", \"since\": \"abc123\"}\n```\n\n응답:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_entries\",\n  \"success\": true,\n  \"data\": {\n    \"entries\": [\n      {\"type\": \"message\", \"id\": \"def456\", \"parentId\": \"abc123\", \"timestamp\": \"...\", \"message\": {\"role\": \"user\", \"...\": \"...\"}}\n    ],\n    \"leafId\": \"def456\"\n  }\n}\n```\n\n`leafId`는 현재 리프 항목의 ID(빈 세션의 경우 `null`)이므로 클라이언트는 활성 분기가 이동했는지 여부를 한 번의 왕복으로 알 수 있습니다. `since`가 항목 ID와 일치하지 않는 경우 응답은 `success: false`입니다.\n\n#### get_tree\n\n세션을 항목 트리로 가져옵니다. 각 노드는 `{entry, children, label?, labelTimestamp?}`입니다. 잘 구성된 세션에는 단일 루트가 있습니다. 고아 항목(깨진 상위 체인)도 루트로 나타납니다.\n\n```json\n{\"type\": \"get_tree\"}\n```\n\n응답:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_tree\",\n  \"success\": true,\n  \"data\": {\n    \"tree\": [\n      {\n        \"entry\": {\"type\": \"message\", \"id\": \"abc123\", \"parentId\": null, \"...\": \"...\"},\n        \"children\": [\n          {\"entry\": {\"type\": \"message\", \"id\": \"def456\", \"parentId\": \"abc123\", \"...\": \"...\"}, \"children\": []}\n        ]\n      }\n    ],\n    \"leafId\": \"def456\"\n  }\n}\n```\n\n#### get_last_assistant_text\n\n마지막 보조 메시지의 텍스트 콘텐츠를 가져옵니다.\n\n```json\n{\"type\": \"get_last_assistant_text\"}\n```\n\n응답:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_last_assistant_text\",\n  \"success\": true,\n  \"data\": {\"text\": \"The assistant's response...\"}\n}\n```\n\n보조 메시지가 없으면 `{\"text\": null}`를 반환합니다.\n\n#### 세트_세션_이름\n\n현재 세션의 표시 이름을 설정합니다. 이름은 세션 목록에 나타나며 세션을 식별하는 데 도움이 됩니다.\n\n```json\n{\"type\": \"set_session_name\", \"name\": \"my-feature-work\"}\n```\n\n응답:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"set_session_name\",\n  \"success\": true\n}\n```\n\n현재 세션 이름은 `sessionName` 필드의 `get_state`를 통해 확인할 수 있습니다. RPC 모드 시작 시 초기 이름을 설정하려면 `--name <name>` 또는 `-n <name>`를 `pi --mode rpc` 프로세스에 전달합니다.\n\n### 명령\n\n#### get_commands\n\n사용 가능한 명령(확장 명령, prompt templates 및 스킬)을 가져옵니다. `/` 접두어를 붙여 `prompt` 명령을 통해 호출할 수 있습니다.\n\n```json\n{\"type\": \"get_commands\"}\n```\n\n응답:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_commands\",\n  \"success\": true,\n  \"data\": {\n    \"commands\": [\n      {\"name\": \"session-name\", \"description\": \"Set or clear session name\", \"source\": \"extension\", \"path\": \"/home/user/.pi/agent/extensions/session.ts\"},\n      {\"name\": \"fix-tests\", \"description\": \"Fix failing tests\", \"source\": \"prompt\", \"location\": \"project\", \"path\": \"/home/user/myproject/.pi/agent/prompts/fix-tests.md\"},\n      {\"name\": \"skill:brave-search\", \"description\": \"Web search via Brave API\", \"source\": \"skill\", \"location\": \"user\", \"path\": \"/home/user/.pi/agent/skills/brave-search/SKILL.md\"}\n    ]\n  }\n}\n```\n\n각 명령에는 다음이 포함됩니다.\n- `name`: 명령 이름(`/name`로 호출)\n- `description`: 사람이 읽을 수 있는 설명(확장 명령의 경우 선택 사항)\n- `source`: 어떤 종류의 명령인지:\n  - `\"extension\"`: 확장 프로그램의 `pi.registerCommand()`을 통해 등록됨\n  - `\"prompt\"`: 프롬프트 템플릿 `.md` 파일에서 로드됨\n  - `\"skill\"`: 스킬 디렉터리에서 로드됨(이름 앞에 `skill:`이 붙음)\n- `location`: 로드된 위치(선택 사항, 확장 기능에는 없음):\n  - `\"user\"`: 사용자 수준(`~/.pi/agent/`)\n  - `\"project\"`: 프로젝트 수준(`./.pi/agent/`)\n  - `\"path\"`: CLI 또는 설정을 통한 명시적 경로\n- `path`: 명령 소스의 절대 파일 경로(선택 사항)\n\n**참고**: 내장 TUI 명령(`/settings`, `/hotkeys` 등)은 포함되지 않습니다. 대화형 모드에서만 처리되며 `prompt`를 통해 전송되면 실행되지 않습니다.\n\n## 이벤트\n\n에이전트 작동 중에는 이벤트가 JSON 라인으로 stdout로 스트리밍됩니다. 이벤트에는 일반적으로 `id` 필드가 포함되지 않습니다. `bash_execution_update`에는 제공된 `bash` 명령의 `id`가 포함됩니다.\n\n### 이벤트 유형\n\n| 이벤트 | 설명 |\n|-------|-------------|\n| `agent_start` | 에이전트가 처리를 시작합니다. |\n| `agent_end` | 하나의 낮은 수준 에이전트 실행이 완료됩니다(계속 재시도, 압축 또는 대기 중인 연속 작업이 이어질 수 있음). |\n| `agent_settled` | 에이전트 실행이 완전히 완료되었습니다. 자동 재시도, 압축 재시도 또는 대기 중인 연속이 남아 있지 않습니다. |\n| `turn_start` | 새로운 턴이 시작됩니다 |\n| `turn_end` | 회전 완료(보조 메시지 및 도구 결과 포함) |\n| `message_start` | 메시지가 시작됩니다 |\n| `message_update` | 스트리밍 업데이트(텍스트/사고/도구 호출 델타) |\n| `message_end` | 메시지가 완료되었습니다. |\n| `bash_execution_update` | 직접 RPC bash 명령 출력 청크 |\n| `tool_execution_start` | 도구 실행 시작 |\n| `tool_execution_update` | 도구 실행 진행(스트리밍 출력) |\n| `tool_execution_end` | 도구 완료 |\n| `queue_update` | 보류 중인 조정/후속 조치 대기열이 변경되었습니다. |\n| `compaction_start` | 압축이 시작됩니다 |\n| `compaction_end` | 압축이 완료되었습니다. |\n| `auto_retry_start` | 자동 재시도 시작(일시적인 오류 발생 후) |\n| `auto_retry_end` | 자동 재시도 완료(성공 또는 최종 실패) |\n| `summarization_retry_scheduled` | 일시적인 압축 또는 분기 요약 요약 오류로 인해 재시도가 예약되었습니다. |\n| `summarization_retry_attempt_start` | 재시도된 요약 요청이 시작됩니다. |\n| `summarization_retry_finished` | 요약 재시도 루프가 완료되었습니다. |\n| `extension_error` | 확장 프로그램에서 오류가 발생했습니다. |\n\n### 에이전트_시작\n\n에이전트가 프롬프트 처리를 시작할 때 발생합니다.\n\n```json\n{\"type\": \"agent_start\"}\n```\n\n### 에이전트_엔드\n\n하나의 하위 수준 에이전트 실행이 완료되면 발생합니다. 이 실행 중에 생성된 모든 메시지를 포함합니다. `willRetry`가 true이면 자동 재시도가 수행됩니다.\n\n```json\n{\n  \"type\": \"agent_end\",\n  \"messages\": [...],\n  \"willRetry\": false\n}\n```\n\n### 에이전트_정착\n\n전체 세션 수준 실행이 완료된 후 내보냅니다. 이 시점에서 Pi는 재시도, 압축 재시도 또는 대기 중인 후속 메시지를 통해 자동으로 계속되지 않습니다.\n\n```json\n{\"type\": \"agent_settled\"}\n```\n\n### 턴_시작 / 턴_엔드\n\n차례는 하나의 보조자 응답과 그에 따른 도구 호출 및 결과로 구성됩니다.\n\n```json\n{\"type\": \"turn_start\"}\n```\n\n```json\n{\n  \"type\": \"turn_end\",\n  \"message\": {...},\n  \"toolResults\": [...]\n}\n```\n\n### message_start / message_end\n\n메시지가 시작되고 완료될 때 발생합니다. `message` 필드에는 `AgentMessage`이 포함되어 있습니다.\n\n```json\n{\"type\": \"message_start\", \"message\": {...}}\n{\"type\": \"message_end\", \"message\": {...}}\n```\n\n### message_update(스트리밍)\n\n보조 메시지 스트리밍 중에 발생합니다. 누적 메시지 스냅샷 없이 델타 이벤트를 포함합니다.\n\n```json\n{\n  \"type\": \"message_update\",\n  \"assistantMessageEvent\": {\n    \"type\": \"text_delta\",\n    \"contentIndex\": 0,\n    \"delta\": \"Hello \"\n  }\n}\n```\n\n`assistantMessageEvent` 필드에는 다음 델타 유형 중 하나가 포함됩니다.\n\n| 유형 | 설명 |\n|------|-------------|\n| `text_start` | 텍스트 콘텐츠 차단이 시작되었습니다. |\n| `text_delta` | 텍스트 콘텐츠 청크 |\n| `text_end` | 텍스트 콘텐츠 차단이 종료되었습니다. |\n| `thinking_start` | 생각의 블록이 시작되었습니다 |\n| `thinking_delta` | 생각하는 콘텐츠 덩어리 |\n| `thinking_end` | 생각의 블록이 끝났습니다 |\n| `toolcall_start` | 도구 통화가 시작되었습니다. |\n| `toolcall_delta` | 도구 호출 인수 청크 |\n| `toolcall_end` | 도구 호출이 종료되었습니다(전체 `toolCall` 개체 포함) |\n\n텍스트 응답 스트리밍 예시:\n```json\n{\"type\":\"message_update\",\"assistantMessageEvent\":{\"type\":\"text_start\",\"contentIndex\":0}}\n{\"type\":\"message_update\",\"assistantMessageEvent\":{\"type\":\"text_delta\",\"contentIndex\":0,\"delta\":\"Hello\"}}\n{\"type\":\"message_update\",\"assistantMessageEvent\":{\"type\":\"text_delta\",\"contentIndex\":0,\"delta\":\" world\"}}\n{\"type\":\"message_update\",\"assistantMessageEvent\":{\"type\":\"text_end\",\"contentIndex\":0,\"content\":\"Hello world\"}}\n```\n\n`message_update` 이전 누적 `message` 필드를 의도적으로 생략하고\n`assistantMessageEvent.partial`. 실시간 부분 메시지가 필요한 클라이언트는 이를 조합해야 합니다.\n`message_start` 및 `contentIndex`을 사용하는 후속 이벤트에서. 치료 `message_end.message`\n권위 있는 것처럼. 도구 호출의 경우 버퍼 `toolcall_delta.delta`; `toolcall_end.toolCall`\n완료된 통화가 포함되어 있습니다.\n\n### bash_execution_update\n\n직접 `bash` 명령의 각 출력 청크에 대해 한 번씩 내보냅니다. `id`은 명령의 `id`와 일치하므로 클라이언트가 출력을 올바른 명령과 연결할 수 있습니다.\n\n최종 `bash` 응답의 `output`이 잘린 경우에도 이벤트는 명령이 실행되는 동안 모든 출력을 스트리밍합니다.\n\n```json\n{\n  \"type\": \"bash_execution_update\",\n  \"id\": \"req-1\",\n  \"delta\": \"total 48\\n\"\n}\n```\n\n### tool_execution_start / tool_execution_update / tool_execution_end\n\n도구가 시작되고, 진행 상황을 스트리밍하고, 실행이 완료될 때 발생합니다.\n\n```json\n{\n  \"type\": \"tool_execution_start\",\n  \"toolCallId\": \"call_abc123\",\n  \"toolName\": \"bash\",\n  \"args\": {\"command\": \"ls -la\"}\n}\n```\n\n실행 중에 `tool_execution_update` 이벤트는 부분 결과를 스트리밍합니다(예: bash 도착 시 출력).\n\n```json\n{\n  \"type\": \"tool_execution_update\",\n  \"toolCallId\": \"call_abc123\",\n  \"toolName\": \"bash\",\n  \"args\": {\"command\": \"ls -la\"},\n  \"partialResult\": {\n    \"content\": [{\"type\": \"text\", \"text\": \"partial output so far...\"}],\n    \"details\": {\"truncation\": null, \"fullOutputPath\": null}\n  }\n}\n```\n\n완료되면:\n\n```json\n{\n  \"type\": \"tool_execution_end\",\n  \"toolCallId\": \"call_abc123\",\n  \"toolName\": \"bash\",\n  \"result\": {\n    \"content\": [{\"type\": \"text\", \"text\": \"total 48\\n...\"}],\n    \"details\": {...}\n  },\n  \"isError\": false\n}\n```\n\n이벤트를 연관시키려면 `toolCallId`를 사용하세요. `tool_execution_update`의 `partialResult`에는 (델타뿐만 아니라) 지금까지 누적된 출력이 포함되어 클라이언트가 업데이트할 때마다 디스플레이를 간단히 교체할 수 있습니다.\n\n### 대기열_업데이트\n\n보류 중인 조정 또는 후속 조치 대기열이 변경될 때마다 발생합니다.\n\n```json\n{\n  \"type\": \"queue_update\",\n  \"steering\": [\"Focus on error handling\"],\n  \"followUp\": [\"After that, summarize the result\"]\n}\n```\n\n### 압축_시작 / 압축_끝\n\n수동이든 자동이든 압축이 실행될 때 발생합니다.\n\n```json\n{\"type\": \"compaction_start\", \"reason\": \"threshold\"}\n```\n\n`reason` 필드는 `\"manual\"`, `\"threshold\"` 또는 `\"overflow\"`입니다.\n\n```json\n{\n  \"type\": \"compaction_end\",\n  \"reason\": \"threshold\",\n  \"result\": {\n    \"summary\": \"Summary of conversation...\",\n    \"firstKeptEntryId\": \"abc123\",\n    \"tokensBefore\": 150000,\n    \"estimatedTokensAfter\": 32000,\n    \"usage\": {\n      \"input\": 32000,\n      \"output\": 1200,\n      \"cacheRead\": 0,\n      \"cacheWrite\": 0,\n      \"totalTokens\": 33200,\n      \"cost\": {\"input\": 0.01, \"output\": 0.02, \"cacheRead\": 0, \"cacheWrite\": 0, \"total\": 0.03}\n    },\n    \"details\": {}\n  },\n  \"aborted\": false,\n  \"willRetry\": false\n}\n```\n\n`reason`가 `\"overflow\"`이고 압축에 성공한 경우 `willRetry`는 `true`이고 에이전트는 자동으로 프롬프트를 다시 시도합니다.\n\n압축이 중단된 경우 `result`는 `null`이고 `aborted`는 `true`입니다.\n\n압축에 실패한 경우(예: API 할당량 초과) `result`는 `null`, `aborted`는 `false`, `errorMessage`에는 오류 설명이 포함됩니다.\n\n### auto_retry_start / auto_retry_end\n\n일시적 오류(오버로드, 속도 제한, 5xx) 후 자동 재시도가 트리거될 때 발생합니다.\n\n```json\n{\n  \"type\": \"auto_retry_start\",\n  \"attempt\": 1,\n  \"maxAttempts\": 3,\n  \"delayMs\": 2000,\n  \"errorMessage\": \"529 {\\\"type\\\":\\\"error\\\",\\\"error\\\":{\\\"type\\\":\\\"overloaded_error\\\",\\\"message\\\":\\\"Overloaded\\\"}}\"\n}\n```\n\n```json\n{\n  \"type\": \"auto_retry_end\",\n  \"success\": true,\n  \"attempt\": 2\n}\n```\n\n최종 실패 시(최대 재시도 횟수 초과):\n```json\n{\n  \"type\": \"auto_retry_end\",\n  \"success\": false,\n  \"attempt\": 3,\n  \"finalError\": \"529 overloaded_error: Overloaded\"\n}\n```\n\n### summarization_retry_scheduled / summarization_retry_attempt_start / summarization_retry_finished\n\n일시적인 공급자 오류 후 압축 또는 분기 요약 요약을 다시 시도할 때 발생합니다. 이러한 이벤트는 자동 보조자 전환 재시도와 동일한 재시도 설정을 사용합니다.\n\n```json\n{\n  \"type\": \"summarization_retry_scheduled\",\n  \"attempt\": 1,\n  \"maxAttempts\": 3,\n  \"delayMs\": 2000,\n  \"errorMessage\": \"terminated\"\n}\n```\n\n```json\n{\n  \"type\": \"summarization_retry_attempt_start\",\n  \"source\": \"compaction\",\n  \"reason\": \"threshold\"\n}\n```\n\n분기 요약의 경우 `source`는 `\"branchSummary\"`이고 `reason`는 없습니다.\n\n```json\n{\n  \"type\": \"summarization_retry_finished\"\n}\n```\n\n### 확장_오류\n\n확장 프로그램에서 오류가 발생하면 발생합니다.\n\n```json\n{\n  \"type\": \"extension_error\",\n  \"extensionPath\": \"/path/to/extension.ts\",\n  \"event\": \"tool_call\",\n  \"error\": \"Error message...\"\n}\n```\n\n## 확장 UI 프로토콜\n\nExtensions는 `ctx.ui.select()`, `ctx.ui.confirm()` 등을 통해 사용자 상호 작용을 요청할 수 있습니다. RPC 모드에서는 이러한 상호 작용이 기본 명령/이벤트 흐름 위에 있는 요청/응답 하위 프로토콜로 변환됩니다.\n\n확장 UI 메서드에는 두 가지 범주가 있습니다.\n\n- **대화 상자 방법**(`select`, `confirm`, `input`, `editor`): stdout에서 `extension_ui_request`를 내보내고 클라이언트가 일치하는 `id`와 함께 stdin에서 `extension_ui_response`를 다시 보낼 때까지 차단합니다.\n- **Fire-and-forget 방법**(`notify`, `setStatus`, `setWidget`, `setTitle`, `set_editor_text`): stdout에서 `extension_ui_request`를 내보내지만 응답을 기대하지 않습니다. 클라이언트는 정보를 표시하거나 무시할 수 있습니다.\n\n대화 메서드에 `timeout` 필드가 포함된 경우 에이전트 측은 제한 시간이 만료되면 기본값을 사용하여 자동 해결됩니다. 클라이언트는 시간 초과를 추적할 필요가 없습니다.\n\n일부 `ExtensionUIContext` 메소드는 직접 TUI 액세스가 필요하기 때문에 RPC 모드에서 지원되지 않거나 성능이 저하됩니다.\n- `custom()` 반환 `undefined`\n- `setWorkingMessage()`, `setWorkingIndicator()`, `setFooter()`, `setHeader()`, `setEditorComponent()`, `setToolsExpanded()`는 작동하지 않습니다.\n- `getEditorText()` 반환 `\"\"`\n- `getToolsExpanded()` 반환 `false`\n- `pasteToEditor()` `setEditorText()`에 위임(붙여넣기/접기 처리 없음)\n- `getAllThemes()` 반환 `[]`\n- `getTheme()` 반환 `undefined`\n- `setTheme()` 반환 `{ success: false, error: \"...\" }`\n\n참고: `ctx.mode`는 `\"rpc\"`이고 `ctx.hasUI`는 RPC 모드에서 `true`입니다. 왜냐하면 대화 상자 및 실행 후 잊어버리기 메서드가 확장 UI 하위 프로토콜을 통해 작동하기 때문입니다. 실제 터미널이 필요한 `custom()`와 같은 TUI 특정 기능을 보호하려면 `ctx.mode === \"tui\"`를 사용하세요.\n\n### 확장 UI 요청(stdout)\n\n모든 요청에는 `type: \"extension_ui_request\"`, 고유한 `id` 및 `method` 필드가 있습니다.\n\n#### 선택하다\n\n사용자에게 목록에서 선택하라는 메시지를 표시합니다. `timeout` 필드가 있는 대화 상자 메서드에는 밀리초 단위의 시간 제한이 포함됩니다. 클라이언트가 시간 내에 응답하지 않으면 에이전트는 `undefined`로 자동 해결됩니다.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-1\",\n  \"method\": \"select\",\n  \"title\": \"Allow dangerous command?\",\n  \"options\": [\"Allow\", \"Block\"],\n  \"timeout\": 10000\n}\n```\n\n예상 응답: `extension_ui_response` + `value`(선택한 옵션 문자열) 또는 `cancelled: true`.\n\n#### 확인하다\n\n사용자에게 예/아니요 확인 메시지를 표시합니다.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-2\",\n  \"method\": \"confirm\",\n  \"title\": \"Clear session?\",\n  \"message\": \"All messages will be lost.\",\n  \"timeout\": 5000\n}\n```\n\n예상 응답: `extension_ui_response` 및 `confirmed: true/false` 또는 `cancelled: true`.\n\n#### 입력\n\n사용자에게 자유 형식 텍스트를 요청합니다.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-3\",\n  \"method\": \"input\",\n  \"title\": \"Enter a value\",\n  \"placeholder\": \"type something...\"\n}\n```\n\n예상 응답: `extension_ui_response` + `value`(입력한 텍스트) 또는 `cancelled: true`.\n\n#### 편집자\n\n선택적으로 미리 채워진 콘텐츠가 포함된 여러 줄 텍스트 편집기를 엽니다.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-4\",\n  \"method\": \"editor\",\n  \"title\": \"Edit some text\",\n  \"prefill\": \"Line 1\\nLine 2\\nLine 3\"\n}\n```\n\n예상 응답: `extension_ui_response` + `value`(수정된 텍스트) 또는 `cancelled: true`.\n\n#### 통지하다\n\n알림을 표시합니다. 실행 후 잊어버리면 응답이 예상되지 않습니다.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-5\",\n  \"method\": \"notify\",\n  \"message\": \"Command blocked by user\",\n  \"notifyType\": \"warning\"\n}\n```\n\n`notifyType` 필드는 `\"info\"`, `\"warning\"` 또는 `\"error\"`입니다. 생략할 경우 기본값은 `\"info\"`입니다.\n\n#### setStatus\n\n바닥글/상태 표시줄의 상태 항목을 설정하거나 지웁니다. 실행 후 잊어버리세요.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-6\",\n  \"method\": \"setStatus\",\n  \"statusKey\": \"my-ext\",\n  \"statusText\": \"Turn 3 running...\"\n}\n```\n\n해당 키의 상태 항목을 지우려면 `statusText: undefined`(또는 생략)을 전송하세요.\n\n#### setWidget\n\n편집기 위나 아래에 표시되는 위젯(텍스트 줄 블록)을 설정하거나 지웁니다. 실행 후 잊어버리세요.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-7\",\n  \"method\": \"setWidget\",\n  \"widgetKey\": \"my-ext\",\n  \"widgetLines\": [\"--- My Widget ---\", \"Line 1\", \"Line 2\"],\n  \"widgetPlacement\": \"aboveEditor\"\n}\n```\n\n위젯을 지우려면 `widgetLines: undefined`(또는 생략)을 보내세요. `widgetPlacement` 필드는 `\"aboveEditor\"`(기본값) 또는 `\"belowEditor\"`입니다. RPC 모드에서는 문자열 배열만 지원됩니다. 구성요소 팩토리는 무시됩니다.\n\n#### 제목 설정\n\n터미널 창/탭 제목을 설정합니다. 실행 후 잊어버리세요.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-8\",\n  \"method\": \"setTitle\",\n  \"title\": \"pi - my project\"\n}\n```\n\n#### set_editor_text\n\n입력 편집기에서 텍스트를 설정합니다. 실행 후 잊어버리세요.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-9\",\n  \"method\": \"set_editor_text\",\n  \"text\": \"prefilled text for the user\"\n}\n```\n\n### 확장 UI 응답(stdin)\n\n응답은 대화 상자 메서드(`select`, `confirm`, `input`, `editor`)에 대해서만 전송됩니다. `id`는 요청과 일치해야 합니다.\n\n#### 값 응답(선택, 입력, 편집기)\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-1\", \"value\": \"Allow\"}\n```\n\n#### 확인응답(확인)\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-2\", \"confirmed\": true}\n```\n\n#### 취소 응답(모든 대화상자)\n\n대화 상자 메서드를 닫습니다. 확장 프로그램은 `undefined`(선택/입력/편집용) 또는 `false`(확인용)을 수신합니다.\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-3\", \"cancelled\": true}\n```\n\n## 오류 처리\n\n실패한 명령은 `success: false`로 응답을 반환합니다.\n\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"set_model\",\n  \"success\": false,\n  \"error\": \"Model not found: invalid/model\"\n}\n```\n\n구문 분석 오류:\n\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"parse\",\n  \"success\": false,\n  \"error\": \"Failed to parse command: Unexpected token...\"\n}\n```\n\n## 유형\n\n소스 파일:\n- [`packages/ai/src/types.ts`](../../ai/src/types.ts) - `Model`, `UserMessage`, `AssistantMessage`, `ToolResultMessage`\n- [`packages/agent/src/types.ts`](../../agent/src/types.ts) - `AgentMessage`, `AgentEvent`\n- [`src/core/messages.ts`](../src/core/messages.ts) - `BashExecutionMessage`\n- [`src/modes/json-event.ts`](../src/modes/json-event.ts) - `JsonAgentSessionEvent`\n- [`src/modes/rpc/rpc-types.ts`](../src/modes/rpc/rpc-types.ts) - RPC 명령/응답 유형, 확장 UI 요청/응답 유형\n\n### 모델\n\n```json\n{\n  \"id\": \"claude-sonnet-4-20250514\",\n  \"name\": \"Claude Sonnet 4\",\n  \"api\": \"anthropic-messages\",\n  \"provider\": \"anthropic\",\n  \"baseUrl\": \"https://api.anthropic.com\",\n  \"reasoning\": true,\n  \"input\": [\"text\", \"image\"],\n  \"contextWindow\": 200000,\n  \"maxTokens\": 16384,\n  \"cost\": {\n    \"input\": 3.0,\n    \"output\": 15.0,\n    \"cacheRead\": 0.3,\n    \"cacheWrite\": 3.75\n  }\n}\n```\n\n### 사용자 메시지\n\n```json\n{\n  \"role\": \"user\",\n  \"content\": \"Hello!\",\n  \"timestamp\": 1733234567890,\n  \"attachments\": []\n}\n```\n\n`content` 필드는 문자열이거나 `TextContent`/`ImageContent` 블록 배열일 수 있습니다.\n\n### 어시스턴트 메시지\n\n```json\n{\n  \"role\": \"assistant\",\n  \"content\": [\n    {\"type\": \"text\", \"text\": \"Hello! How can I help?\"},\n    {\"type\": \"thinking\", \"thinking\": \"User is greeting me...\"},\n    {\"type\": \"toolCall\", \"id\": \"call_123\", \"name\": \"bash\", \"arguments\": {\"command\": \"ls\"}}\n  ],\n  \"api\": \"anthropic-messages\",\n  \"provider\": \"anthropic\",\n  \"model\": \"claude-sonnet-4-20250514\",\n  \"usage\": {\n    \"input\": 100,\n    \"output\": 50,\n    \"cacheRead\": 0,\n    \"cacheWrite\": 0,\n    \"cost\": {\"input\": 0.0003, \"output\": 0.00075, \"cacheRead\": 0, \"cacheWrite\": 0, \"total\": 0.00105}\n  },\n  \"stopReason\": \"stop\",\n  \"timestamp\": 1733234567890\n}\n```\n\n중지 이유: `\"stop\"`, `\"length\"`, `\"toolUse\"`, `\"error\"`, `\"aborted\"`\n\n### 도구결과메시지\n\n```json\n{\n  \"role\": \"toolResult\",\n  \"toolCallId\": \"call_123\",\n  \"toolName\": \"bash\",\n  \"content\": [{\"type\": \"text\", \"text\": \"total 48\\ndrwxr-xr-x ...\"}],\n  \"usage\": {\n    \"input\": 100,\n    \"output\": 50,\n    \"cacheRead\": 0,\n    \"cacheWrite\": 0,\n    \"totalTokens\": 150,\n    \"cost\": {\"input\": 0.0003, \"output\": 0.00075, \"cacheRead\": 0, \"cacheWrite\": 0, \"total\": 0.00105}\n  },\n  \"isError\": false,\n  \"timestamp\": 1733234567890\n}\n```\n\n`usage`는 선택 사항이며 도구에서 수행된 중첩된 LLM 작업을 보고합니다. 존재하는 경우 세션 토큰 및 총 비용에 기여합니다.\n\n### Bash실행메시지\n\n`bash` RPC 명령으로 생성됨(LLM 도구 호출이 아님):\n\n```json\n{\n  \"role\": \"bashExecution\",\n  \"command\": \"ls -la\",\n  \"output\": \"total 48\\ndrwxr-xr-x ...\",\n  \"exitCode\": 0,\n  \"cancelled\": false,\n  \"truncated\": false,\n  \"fullOutputPath\": null,\n  \"timestamp\": 1733234567890\n}\n```\n\n### 부착\n\n```json\n{\n  \"id\": \"img1\",\n  \"type\": \"image\",\n  \"fileName\": \"photo.jpg\",\n  \"mimeType\": \"image/jpeg\",\n  \"size\": 102400,\n  \"content\": \"base64-encoded-data...\",\n  \"extractedText\": null,\n  \"preview\": null\n}\n```\n\n## 예: 기본 클라이언트(Python)\n\n```python\nimport subprocess\nimport json\n\nproc = subprocess.Popen(\n    [\"pi\", \"--mode\", \"rpc\", \"--no-session\"],\n    stdin=subprocess.PIPE,\n    stdout=subprocess.PIPE,\n    text=True\n)\n\ndef send(cmd):\n    proc.stdin.write(json.dumps(cmd) + \"\\n\")\n    proc.stdin.flush()\n\ndef read_events():\n    for line in proc.stdout:\n        yield json.loads(line)\n\n# Send prompt\nsend({\"type\": \"prompt\", \"message\": \"Hello!\"})\n\n# Process events\nfor event in read_events():\n    if event.get(\"type\") == \"message_update\":\n        delta = event.get(\"assistantMessageEvent\", {})\n        if delta.get(\"type\") == \"text_delta\":\n            print(delta[\"delta\"], end=\"\", flush=True)\n    \n    if event.get(\"type\") == \"agent_end\":\n        print()\n        break\n```\n\n## 예: 대화형 클라이언트(Node.js)\n\n완전한 대화형 예제는 [`test/rpc-example.ts`](../test/rpc-example.ts)를 참조하고, 형식화된 클라이언트 구현은 [`src/modes/rpc/rpc-client.ts`](../src/modes/rpc/rpc-client.ts)를 참조하세요.\n\n확장 UI 프로토콜을 처리하는 전체 예를 보려면 [`examples/extensions/rpc-demo.ts`](../examples/extensions/rpc-demo.ts) 확장과 쌍을 이루는 [`examples/rpc-extension-ui.ts`](../examples/rpc-extension-ui.ts)를 참조하세요.\n\n```javascript\nconst { spawn } = require(\"child_process\");\nconst { StringDecoder } = require(\"string_decoder\");\n\nconst agent = spawn(\"pi\", [\"--mode\", \"rpc\", \"--no-session\"]);\n\nfunction attachJsonlReader(stream, onLine) {\n    const decoder = new StringDecoder(\"utf8\");\n    let buffer = \"\";\n\n    stream.on(\"data\", (chunk) => {\n        buffer += typeof chunk === \"string\" ? chunk : decoder.write(chunk);\n\n        while (true) {\n            const newlineIndex = buffer.indexOf(\"\\n\");\n            if (newlineIndex === -1) break;\n\n            let line = buffer.slice(0, newlineIndex);\n            buffer = buffer.slice(newlineIndex + 1);\n            if (line.endsWith(\"\\r\")) line = line.slice(0, -1);\n            onLine(line);\n        }\n    });\n\n    stream.on(\"end\", () => {\n        buffer += decoder.end();\n        if (buffer.length > 0) {\n            onLine(buffer.endsWith(\"\\r\") ? buffer.slice(0, -1) : buffer);\n        }\n    });\n}\n\nattachJsonlReader(agent.stdout, (line) => {\n    const event = JSON.parse(line);\n\n    if (event.type === \"message_update\") {\n        const { assistantMessageEvent } = event;\n        if (assistantMessageEvent.type === \"text_delta\") {\n            process.stdout.write(assistantMessageEvent.delta);\n        }\n    }\n});\n\n// Send prompt\nagent.stdin.write(JSON.stringify({ type: \"prompt\", message: \"Hello\" }) + \"\\n\");\n\n// Abort on Ctrl+C\nprocess.on(\"SIGINT\", () => {\n    agent.stdin.write(JSON.stringify({ type: \"abort\" }) + \"\\n\");\n});\n```","sourceFile":"rpc.md"},"sdk":{"title":"SDK","markdown":"> pi는 SDK를 사용하는 데 도움이 될 수 있습니다. 사용 사례에 맞는 통합 구축을 요청하세요.\n\n\nSDK는 pi의 에이전트 기능에 대한 프로그래밍 방식의 액세스를 제공합니다. 이를 사용하여 다른 애플리케이션에 pi를 내장하거나 사용자 정의 인터페이스를 구축하거나 자동화된 워크플로와 통합할 수 있습니다.\n\n**사용 사례 예시:**\n- 맞춤형 UI 구축(웹, 데스크톱, 모바일)\n- 에이전트 기능을 기존 애플리케이션에 통합\n- 에이전트 추론을 통해 자동화된 파이프라인 생성\n- 하위 에이전트를 생성하는 사용자 정의 도구 구축\n- 프로그래밍 방식으로 에이전트 동작 테스트\n\n최소 제어부터 전체 제어까지의 작업 예는 [examples/sdk/](../examples/sdk/)를 참조하세요.\n\n## 빠른 시작\n\n```typescript\nimport { createAgentSession, ModelRuntime, SessionManager } from \"@earendil-works/pi-coding-agent\";\n\nconst modelRuntime = await ModelRuntime.create();\nconst { session } = await createAgentSession({\n  sessionManager: SessionManager.inMemory(),\n  modelRuntime,\n});\n\nsession.subscribe((event) => {\n  if (event.type === \"message_update\" && event.assistantMessageEvent.type === \"text_delta\") {\n    process.stdout.write(event.assistantMessageEvent.delta);\n  }\n});\n\nawait session.prompt(\"What files are in the current directory?\");\n```\n\n## 설치\n\n```bash\nnpm install @earendil-works/pi-coding-agent\n```\n\nSDK은 기본 패키지에 포함되어 있습니다. 별도의 설치가 필요하지 않습니다.\n\n## 핵심 개념\n\n### createAgentSession()\n\n단일 `AgentSession`에 대한 기본 팩토리 기능입니다.\n\n`createAgentSession()`는 `ResourceLoader`을 사용하여 확장 기능, 기술, prompt templates, 테마 및 context files를 제공합니다. 제공하지 않으면 표준 검색과 함께 `DefaultResourceLoader`를 사용합니다.\n\n```typescript\nimport { createAgentSession, SessionManager } from \"@earendil-works/pi-coding-agent\";\n\n// Minimal: defaults with DefaultResourceLoader\nconst { session } = await createAgentSession();\n\n// Custom: override specific options\nconst { session } = await createAgentSession({\n  model: myModel,\n  tools: [\"read\", \"bash\"],\n  sessionManager: SessionManager.inMemory(),\n});\n```\n\n### 에이전트 세션\n\n세션은 에이전트 수명 주기, 메시지 기록, 모델 상태, 압축 및 이벤트 스트리밍을 관리합니다.\n\n```typescript\ninterface AgentSession {\n  // Send a prompt and wait for completion\n  prompt(text: string, options?: PromptOptions): Promise<void>;\n\n  // Queue messages during streaming\n  steer(text: string): Promise<void>;\n  followUp(text: string): Promise<void>;\n\n  // Subscribe to events (returns unsubscribe function)\n  subscribe(listener: (event: AgentSessionEvent) => void): () => void;\n\n  // Session info\n  sessionFile: string | undefined;\n  sessionId: string;\n\n  // Model control\n  setModel(model: Model): Promise<void>;\n  setThinkingLevel(level: ThinkingLevel): void;\n  cycleModel(): Promise<ModelCycleResult | undefined>;\n  cycleThinkingLevel(): ThinkingLevel | undefined;\n\n  // State access\n  agent: Agent;\n  model: Model | undefined;\n  thinkingLevel: ThinkingLevel;\n  messages: AgentMessage[];\n  isStreaming: boolean;\n\n  // In-place tree navigation within the current session file\n  navigateTree(targetId: string, options?: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string }): Promise<{ editorText?: string; cancelled: boolean }>;\n\n  // Compaction\n  compact(customInstructions?: string): Promise<CompactionResult>;\n  abortCompaction(): void;\n\n  // Abort current operation\n  abort(): Promise<void>;\n\n  // Cleanup\n  dispose(): void;\n}\n```\n\n새 세션, 재개, 포크, 가져오기 등의 세션 교체 API는 `AgentSession`이 아닌 `AgentSessionRuntime`에서 라이브로 수행됩니다.\n\n### createAgentSessionRuntime() 및 AgentSessionRuntime\n\n활성 세션을 교체하고 cwd 바인딩 런타임 상태를 다시 빌드해야 하는 경우 런타임 API을 사용하세요.\n이는 내장된 대화형, 인쇄 및 RPC 모드에서 사용되는 것과 동일한 레이어입니다.\n\n`createAgentSessionRuntime()`는 런타임 팩토리와 초기 cwd/세션 대상을 사용합니다. 팩토리는 프로세스 전역 고정 입력을 닫고, 유효 cwd에 대해 cwd 바인딩 서비스를 다시 생성하고, 해당 서비스에 대한 세션 옵션을 확인하고, 전체 런타임 결과를 반환합니다.\n\n```typescript\nimport {\n  type CreateAgentSessionRuntimeFactory,\n  createAgentSessionFromServices,\n  createAgentSessionRuntime,\n  createAgentSessionServices,\n  getAgentDir,\n  SessionManager,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {\n  const services = await createAgentSessionServices({ cwd });\n  return {\n    ...(await createAgentSessionFromServices({\n      services,\n      sessionManager,\n      sessionStartEvent,\n    })),\n    services,\n    diagnostics: services.diagnostics,\n  };\n};\n\nconst runtime = await createAgentSessionRuntime(createRuntime, {\n  cwd: process.cwd(),\n  agentDir: getAgentDir(),\n  sessionManager: SessionManager.create(process.cwd()),\n});\n```\n\n`AgentSessionRuntime`는 다음에서 활성 런타임을 대체합니다.\n\n- `newSession()`\n- `switchSession()`\n- `fork()`\n- `fork(entryId, { position: \"at\" })`를 통한 클론 흐름\n- `importFromJsonl()`\n\n중요한 동작:\n\n- `runtime.session` 해당 작업 후 변경 사항\n- 이벤트 구독은 특정 `AgentSession`에 첨부되므로 교체 후 다시 구독하세요.\n- 확장 기능을 사용하는 경우 새 세션을 위해 `runtime.session.bindExtensions(...)`를 다시 호출하세요.\n- 생성은 `runtime.diagnostics`에 대한 진단을 반환합니다.\n- 런타임 생성 또는 교체가 실패하면 메서드가 발생하고 호출자가 이를 처리하는 방법을 결정합니다.\n\n```typescript\nlet session = runtime.session;\nlet unsubscribe = session.subscribe(() => {});\n\nawait runtime.newSession();\n\nunsubscribe();\nsession = runtime.session;\nunsubscribe = session.subscribe(() => {});\n```\n\n### 프롬프트 및 메시지 큐\n\n`PromptOptions` 프롬프트 확장, 스트리밍 중 대기열 동작 및 프롬프트 실행 전 알림을 제어합니다.\n\n```typescript\ninterface PromptOptions {\n  expandPromptTemplates?: boolean;\n  images?: ImageContent[];\n  streamingBehavior?: \"steer\" | \"followUp\";\n  source?: InputSource;\n  preflightResult?: (success: boolean) => void;\n}\n```\n\n`preflightResult`는 `prompt()` 호출마다 한 번씩 호출됩니다.\n\n- `true` 프롬프트가 수락되거나 대기열에 추가되거나 즉시 처리된 경우\n- `false` 승인 전에 프롬프트 프리플라이트가 거부된 경우\n\n`prompt()` 해결되기 전에 실행됩니다. `prompt()` 재시도를 포함하여 허용된 전체 실행이 완료된 후에만 문제가 해결됩니다. 수락 후 실패는 `preflightResult(false)`를 통하지 않고 일반 이벤트 및 메시지 스트림을 통해 보고됩니다.\n\n`prompt()` 메소드는 prompt templates, 확장 명령 및 메시지 전송을 처리합니다.\n\n```typescript\n// Basic prompt (when not streaming)\nawait session.prompt(\"What files are here?\");\n\n// With images\nawait session.prompt(\"What's in this image?\", {\n  images: [{ type: \"image\", source: { type: \"base64\", mediaType: \"image/png\", data: \"...\" } }]\n});\n\n// During streaming: must specify how to queue the message\nawait session.prompt(\"Stop and do this instead\", { streamingBehavior: \"steer\" });\nawait session.prompt(\"After you're done, also check X\", { streamingBehavior: \"followUp\" });\n```\n\n**행동:**\n- **확장 명령**(예: `/mycommand`): 스트리밍 중에도 즉시 실행됩니다. 그들은 `pi.sendMessage()`를 통해 자체 LLM 상호 작용을 관리합니다.\n- **파일 기반 prompt templates**(`.md` 파일에서): 전송하거나 대기열에 넣기 전의 콘텐츠로 확장됩니다.\n- **`streamingBehavior` 없이 스트리밍하는 동안**: 오류가 발생합니다. `steer()` 또는 `followUp()`를 직접 사용하거나 옵션을 지정하세요.\n- **`preflightResult(true)`**: 프롬프트가 즉시 수락, 대기 또는 처리되었음을 의미합니다.\n- **`preflightResult(false)`**: 수락 전 비행 전 거부를 의미합니다.\n\n스트리밍 중 명시적 대기열의 경우:\n\n```typescript\n// Queue a steering message for delivery after the current assistant turn finishes its tool calls\nawait session.steer(\"New instruction\");\n\n// Wait for agent to finish (delivered only when agent stops)\nawait session.followUp(\"After you're done, also do this\");\n```\n\n`steer()` 및 `followUp()` 모두 파일 기반 prompt templates을 확장하지만 확장 명령에 오류가 발생합니다(확장 명령을 대기열에 추가할 수 없음).\n\n### 에이전트 및 AgentState\n\n`Agent` 클래스(`@earendil-works/pi-agent-core`의)는 핵심 LLM 상호 작용을 처리합니다. `session.agent`를 통해 액세스하세요.\n\n```typescript\n// Access current state\nconst state = session.agent.state;\n\n// state.messages: AgentMessage[] - conversation history\n// state.model: Model - current model\n// state.thinkingLevel: ThinkingLevel - current thinking level\n// state.systemPrompt: string - system prompt\n// state.tools: AgentTool[] - available tools\n// state.streamingMessage?: AgentMessage - current partial assistant message\n// state.errorMessage?: string - latest assistant error\n\n// Replace messages (useful for branching or restoration)\nsession.agent.state.messages = messages; // copies the top-level array\n\n// Replace tools\nsession.agent.state.tools = tools; // copies the top-level array\n\n// Wait for agent to finish processing\nawait session.agent.waitForIdle();\n```\n\n### 이벤트\n\n스트리밍 출력 및 수명 주기 알림을 받으려면 이벤트를 구독하세요.\n\n```typescript\nsession.subscribe((event) => {\n  switch (event.type) {\n    // Streaming text from assistant\n    case \"message_update\":\n      if (event.assistantMessageEvent.type === \"text_delta\") {\n        process.stdout.write(event.assistantMessageEvent.delta);\n      }\n      if (event.assistantMessageEvent.type === \"thinking_delta\") {\n        // Thinking output (if thinking enabled)\n      }\n      break;\n    \n    // Tool execution\n    case \"tool_execution_start\":\n      console.log(`Tool: ${event.toolName}`);\n      break;\n    case \"tool_execution_update\":\n      // Streaming tool output\n      break;\n    case \"tool_execution_end\":\n      console.log(`Result: ${event.isError ? \"error\" : \"success\"}`);\n      break;\n    \n    // Message lifecycle\n    case \"message_start\":\n      // New message starting\n      break;\n    case \"message_end\":\n      // Message complete\n      break;\n    \n    // Agent lifecycle\n    case \"agent_start\":\n      // Agent started processing prompt\n      break;\n    case \"agent_end\":\n      // Agent finished (event.messages contains new messages)\n      break;\n    \n    // Turn lifecycle (one LLM response + tool calls)\n    case \"turn_start\":\n      break;\n    case \"turn_end\":\n      // event.message: assistant response\n      // event.toolResults: tool results from this turn\n      break;\n    \n    // Session events (queue, compaction, retry)\n    case \"queue_update\":\n      console.log(event.steering, event.followUp);\n      break;\n    case \"compaction_start\":\n    case \"compaction_end\":\n    case \"auto_retry_start\":\n    case \"auto_retry_end\":\n    case \"summarization_retry_scheduled\":\n    case \"summarization_retry_attempt_start\":\n    case \"summarization_retry_finished\":\n      break;\n  }\n});\n```\n\n## 옵션 참조\n\n### 디렉토리\n\n```typescript\nconst { session } = await createAgentSession({\n  // Working directory for DefaultResourceLoader discovery\n  cwd: process.cwd(), // default\n  \n  // Global config directory\n  agentDir: \"~/.pi/agent\", // default (expands ~)\n});\n```\n\n`cwd`는 `DefaultResourceLoader`에서 다음 용도로 사용됩니다.\n- 프로젝트 확장(`.pi/extensions/`)\n- 프로젝트 기술:\n  - `.pi/skills/`\n  - `.agents/skills/` `cwd` 및 상위 디렉터리(최대 git repo 루트 또는 repo에 없는 경우 파일 시스템 루트)\n- 프로젝트 프롬프트(`.pi/prompts/`)\n- 컨텍스트 파일(`AGENTS.md` cwd에서 이동)\n- 세션 디렉터리 이름 지정\n\n`agentDir`는 `DefaultResourceLoader`에서 다음 용도로 사용됩니다.\n- 전역 확장(`extensions/`)\n- 글로벌 기술:\n  - `skills/` 아래 `agentDir`(예: `~/.pi/agent/skills/`)\n  - `~/.agents/skills/`\n- 전역 프롬프트(`prompts/`)\n- 전역 컨텍스트 파일(`AGENTS.md`)\n- 설정(`settings.json`)\n- 맞춤 모델(`models.json`)\n- 자격 증명(`auth.json`)\n- 세션(`sessions/`)\n\n사용자 정의 `ResourceLoader`를 전달하면 `cwd` 및 `agentDir`가 더 이상 리소스 검색을 제어하지 않습니다. 이는 여전히 세션 이름 지정 및 도구 경로 해결에 영향을 미칩니다.\n\n### 모델\n\n```typescript\nimport { getModel } from \"@earendil-works/pi-ai\";\nimport { ModelRuntime } from \"@earendil-works/pi-coding-agent\";\n\nconst modelRuntime = await ModelRuntime.create();\n\n// Find specific built-in model (doesn't check if API key exists)\nconst opus = getModel(\"anthropic\", \"claude-opus-4-5\");\nif (!opus) throw new Error(\"Model not found\");\n\n// Find any model by provider/id, including custom models from models.json\n// (doesn't check if API key exists)\nconst customModel = modelRuntime.getModel(\"my-provider\", \"my-model\");\n\n// Get only models that have valid authentication configured\nconst available = await modelRuntime.getAvailable();\n\nconst { session } = await createAgentSession({\n  model: opus,\n  thinkingLevel: \"medium\", // off, minimal, low, medium, high, xhigh, max\n  \n  // Models for cycling (Ctrl+P in interactive mode)\n  scopedModels: [\n    { model: opus, thinkingLevel: \"high\" },\n    { model: haiku, thinkingLevel: \"off\" },\n  ],\n  \n  modelRuntime,\n});\n```\n\n모델이 제공되지 않은 경우:\n1. 세션에서 복원을 시도합니다(계속하는 경우).\n2. 설정의 기본값을 사용합니다.\n3. 첫 번째 사용 가능한 모델로 돌아갑니다.\n\nCLI 모델 구문 분석을 일치시키려면 내보낸 확인 도우미를 사용하세요.\n\n```typescript\nimport {\n  resolveCliModel,\n  resolveModelScopeWithDiagnostics,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst cliModel = resolveCliModel({\n  cliModel: \"anthropic/claude-opus-4-5:high\",\n  modelRuntime,\n});\nif (cliModel.error) throw new Error(cliModel.error);\nif (cliModel.warning) console.warn(cliModel.warning);\n\nconst { scopedModels, diagnostics } = await resolveModelScopeWithDiagnostics(\n  [\"anthropic/*:high\", \"gpt-5\"],\n  modelRuntime,\n);\nfor (const diagnostic of diagnostics) {\n  console.warn(diagnostic.message);\n}\n```\n\n`resolveCliModel()`는 등록된 모든 모델을 사용하므로 `--api-key` 스타일 최초 설정은 저장된 인증이 존재하기 전에 모델을 해결할 수 있습니다. `resolveModelScopeWithDiagnostics()`는 `--models` 및 `enabledModels` 의미와 일치하며 경고를 인쇄하는 대신 반환합니다.\n\n> [examples/sdk/02-custom-model.ts](../examples/sdk/02-custom-model.ts) 참조\n\n### API 키 및 OAuth\n\n인증 해결 우선순위(`ModelRuntime`에서 처리):\n1. 런타임 재정의(`setRuntimeApiKey`를 통해, 지속되지 않음)\n2. `auth.json`(API keys 또는 OAuth 토큰)에 저장된 자격 증명\n3. 환경 변수(`ANTHROPIC_API_KEY`, `OPENAI_API_KEY` 등)\n4. 대체 확인자(`models.json`의 맞춤 공급자 키용)\n\n```typescript\nimport { InMemoryCredentialStore } from \"@earendil-works/pi-ai\";\nimport { createAgentSession, ModelRuntime } from \"@earendil-works/pi-coding-agent\";\n\n// Default: uses ~/.pi/agent/auth.json and ~/.pi/agent/models.json\nconst modelRuntime = await ModelRuntime.create();\n\n// Provider-owned auth methods and current status\nfor (const provider of modelRuntime.getProviders()) {\n  const status = await modelRuntime.checkAuth(provider.id);\n  console.log(provider.name, provider.auth, status);\n}\n\n// Runtime API key override (not persisted to disk)\nawait modelRuntime.setRuntimeApiKey(\"anthropic\", \"sk-my-temp-key\");\n\n// Custom credential and model locations\nconst customRuntime = await ModelRuntime.create({\n  authPath: \"/my/app/auth.json\",\n  modelsPath: \"/my/app/models.json\",\n});\n\n// Or inject any pi-ai CredentialStore\nconst credentials = new InMemoryCredentialStore();\nconst inMemoryRuntime = await ModelRuntime.create({ credentials });\n\nconst { session } = await createAgentSession({\n  modelRuntime: customRuntime,\n});\n```\n\n`login()`, `logout()`, `setRuntimeApiKey()` 및 `removeRuntimeApiKey()`는 영향을 받는 제공업체의 캐시/내장 카탈로그, 구성 및 가용성 스냅샷이 로컬에서 일관된 후에 해결됩니다. 원격 카탈로그가 최신 상태가 될 때까지 기다리지 않습니다. 자격 증명이 커밋되었지만 로컬 동기화에 실패하면 내보낸 `CredentialSynchronizationError`; 맹목적으로 자격 증명 변형을 재시도하는 대신 `providerId`, `operation`, `credential` 및 `cause` 필드를 검사하세요.\n\n공개 모델/인증 작업 및 `ModelRuntime.create({ signal })`는 선택적 중단 신호를 허용하며 생략 시 제한이 없습니다. SDK 원격 카탈로그 업데이트에 대한 애플리케이션 자체 마감일 정책:\n\n```typescript\nconst signal = AbortSignal.timeout(15_000);\nconst result = await modelRuntime.refresh({\n  providers: [\"anthropic\"],\n  signal,\n});\nif (result.aborted) console.warn(\"Catalog refresh timed out; using cached models\");\nfor (const [providerId, error] of result.errors) {\n  console.warn(`Could not refresh ${providerId}:`, error);\n}\n```\n\n실패하거나 시간이 초과된 네트워크 새로 고침은 성공적인 자격 증명 작업을 취소하지 않습니다. `refresh()` 새로운 공급자 생성을 시작하므로 이전에 지연된 새로 고침 뒤에 기다리지 않으며 오래된 세대는 나중에 게시할 수 없습니다.\n\n> [examples/sdk/09-api-keys-and-oauth.ts](../examples/sdk/09-api-keys-and-oauth.ts) 참조\n\n### 시스템 프롬프트\n\n시스템 프롬프트를 재정의하려면 `ResourceLoader`를 사용하세요.\n\n```typescript\nimport { createAgentSession, DefaultResourceLoader } from \"@earendil-works/pi-coding-agent\";\n\nconst loader = new DefaultResourceLoader({\n  systemPromptOverride: () => \"You are a helpful assistant.\",\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({ resourceLoader: loader });\n```\n\n> [examples/sdk/03-custom-prompt.ts](../examples/sdk/03-custom-prompt.ts) 참조\n\n### 도구\n\n활성화할 내장 도구를 지정합니다.\n\n- 내장 도구 이름: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`\n- 기본 내장: `read`, `bash`, `edit`, `write`\n- `noTools: \"all\"` 모든 도구를 비활성화합니다.\n- `noTools: \"builtin\"` 확장 기능과 맞춤 도구는 활성화된 상태로 유지하면서 기본 내장 기능을 비활성화합니다.\n- `excludeTools` `tools` 허용 목록이 적용된 후 특정 내장, 확장 또는 맞춤 도구 이름을 비활성화합니다.\n\n`edit` 도구는 Pi의 TUI 디스플레이에 대해 `details.diff`를 반환하고 SDK 소비자를 위한 표준 통합 패치로 `details.patch`를 반환합니다.\n\n```typescript\nimport { createAgentSession } from \"@earendil-works/pi-coding-agent\";\n\n// Read-only mode\nconst { session } = await createAgentSession({\n  tools: [\"read\", \"grep\", \"find\", \"ls\"],\n});\n\n// Pick specific tools\nconst { session } = await createAgentSession({\n  tools: [\"read\", \"bash\", \"grep\"],\n});\n\n// Disable one tool while keeping the rest available\nconst { session } = await createAgentSession({\n  excludeTools: [\"ask_question\"],\n});\n```\n\n#### 사용자 정의 cwd가 포함된 도구\n\n사용자 정의 `cwd`를 전달하면 `createAgentSession()`는 해당 cwd에 대해 선택된 내장 도구를 빌드합니다.\n\n```typescript\nimport { createAgentSession, SessionManager } from \"@earendil-works/pi-coding-agent\";\n\nconst cwd = \"/path/to/project\";\n\n// Use default tools for custom cwd\nconst { session } = await createAgentSession({\n  cwd,\n  sessionManager: SessionManager.inMemory(cwd),\n});\n\n// Or pick specific tools for custom cwd\nconst { session } = await createAgentSession({\n  cwd,\n  tools: [\"read\", \"bash\", \"grep\"],\n  sessionManager: SessionManager.inMemory(cwd),\n});\n```\n\n> [examples/sdk/05-tools.ts](../examples/sdk/05-tools.ts) 참조\n\n### 맞춤형 도구\n\n```typescript\nimport { Type } from \"typebox\";\nimport { createAgentSession, defineTool } from \"@earendil-works/pi-coding-agent\";\n\n// Inline custom tool\nconst myTool = defineTool({\n  name: \"my_tool\",\n  label: \"My Tool\",\n  description: \"Does something useful\",\n  parameters: Type.Object({\n    input: Type.String({ description: \"Input value\" }),\n  }),\n  execute: async (_toolCallId, params) => ({\n    content: [{ type: \"text\", text: `Result: ${params.input}` }],\n    details: {},\n  }),\n});\n\n// Pass custom tools directly\nconst { session } = await createAgentSession({\n  customTools: [myTool],\n});\n```\n\n`customTools: [myTool]`와 같은 독립형 정의 및 배열에는 `defineTool()`를 사용하세요. 인라인 `pi.registerTool({... })`는 이미 매개변수 유형을 올바르게 추론합니다.\n\n`customTools`를 통해 전달된 사용자 정의 도구는 확장 프로그램에 등록된 도구와 결합됩니다. ResourceLoader에 의해 로드된 Extensions는 `pi.registerTool()`를 통해 도구를 등록할 수도 있습니다.\n\n`tools`를 전달하는 경우 활성화하려는 각 사용자 정의 또는 확장 도구 이름을 포함합니다(예: `tools: [\"read\", \"bash\", \"my_tool\"]`).\n\n> [examples/sdk/05-tools.ts](../examples/sdk/05-tools.ts) 참조\n\n### Extensions\n\nExtensions는 `ResourceLoader`에 의해 로드됩니다. `DefaultResourceLoader`는 `~/.pi/agent/extensions/`, `.pi/extensions/` 및 settings.json 확장 소스에서 확장을 검색합니다.\n\n```typescript\nimport { createAgentSession, DefaultResourceLoader } from \"@earendil-works/pi-coding-agent\";\n\nconst loader = new DefaultResourceLoader({\n  additionalExtensionPaths: [\"/path/to/my-extension.ts\"],\n  extensionFactories: [\n    (pi) => {\n      pi.on(\"agent_start\", () => {\n        console.log(\"[Inline Extension] Agent starting\");\n      });\n    },\n  ],\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({ resourceLoader: loader });\n```\n\nExtensions 도구 등록, 이벤트 구독, 명령 추가 등을 할 수 있습니다. 전체 API를 보려면 [extensions.md](extensions.md)를 참조하세요.\n\n**명명된 인라인 확장:** 기본적으로 인라인 팩토리는 시작 Extensions 목록에 `<inline:1>`, `<inline:2>` 등으로 표시됩니다. 대신 설명적인 이름을 표시하려면 팩토리를 래핑하세요.\n\n```typescript\nimport type { InlineExtension } from \"@earendil-works/pi-coding-agent\";\n\nconst myProvider: InlineExtension = {\n  name: \"my-provider\",\n  factory: (pi) => {\n    pi.on(\"agent_start\", () => {\n      console.log(\"[my-provider] Agent starting\");\n    });\n  },\n};\n\nconst loader = new DefaultResourceLoader({\n  extensionFactories: [myProvider],\n});\n```\n\n`<inline:1>` 대신 `<inline:my-provider>`로 표시됩니다. 이전 버전과의 호환성을 위해 베어 팩토리 기능은 여전히 ​​허용됩니다.\n\n**이벤트 버스:** Extensions는 `pi.events`를 통해 통신할 수 있습니다. 외부에서 내보내거나 들어야 하는 경우 공유 `eventBus`를 `DefaultResourceLoader`로 전달합니다.\n\n```typescript\nimport { createEventBus, DefaultResourceLoader } from \"@earendil-works/pi-coding-agent\";\n\nconst eventBus = createEventBus();\nconst loader = new DefaultResourceLoader({\n  eventBus,\n});\nawait loader.reload();\n\neventBus.on(\"my-extension:status\", (data) => console.log(data));\n```\n\n> [examples/sdk/06-extensions.ts](../examples/sdk/06-extensions.ts) 및 [docs/extensions.md](extensions.md)를 참조하세요.\n\n### Skills\n\n```typescript\nimport {\n  createAgentSession,\n  DefaultResourceLoader,\n  type Skill,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst customSkill: Skill = {\n  name: \"my-skill\",\n  description: \"Custom instructions\",\n  filePath: \"/path/to/SKILL.md\",\n  baseDir: \"/path/to\",\n  source: \"custom\",\n};\n\nconst loader = new DefaultResourceLoader({\n  skillsOverride: (current) => ({\n    skills: [...current.skills, customSkill],\n    diagnostics: current.diagnostics,\n  }),\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({ resourceLoader: loader });\n```\n\n> [examples/sdk/04-skills.ts](../examples/sdk/04-skills.ts) 참조\n\n### 컨텍스트 파일\n\n```typescript\nimport { createAgentSession, DefaultResourceLoader } from \"@earendil-works/pi-coding-agent\";\n\nconst loader = new DefaultResourceLoader({\n  agentsFilesOverride: (current) => ({\n    agentsFiles: [\n      ...current.agentsFiles,\n      { path: \"/virtual/AGENTS.md\", content: \"# Guidelines\\n\\n- Be concise\" },\n    ],\n  }),\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({ resourceLoader: loader });\n```\n\n> [examples/sdk/07-context-files.ts](../examples/sdk/07-context-files.ts) 참조\n\n### 슬래시 명령\n\n```typescript\nimport {\n  createAgentSession,\n  DefaultResourceLoader,\n  type PromptTemplate,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst customCommand: PromptTemplate = {\n  name: \"deploy\",\n  description: \"Deploy the application\",\n  source: \"(custom)\",\n  content: \"# Deploy\\n\\n1. Build\\n2. Test\\n3. Deploy\",\n};\n\nconst loader = new DefaultResourceLoader({\n  promptsOverride: (current) => ({\n    prompts: [...current.prompts, customCommand],\n    diagnostics: current.diagnostics,\n  }),\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({ resourceLoader: loader });\n```\n\n> [examples/sdk/08-prompt-templates.ts](../examples/sdk/08-prompt-templates.ts) 참조\n\n### 세션 관리\n\n세션은 `id`/`parentId` 링크가 포함된 트리 구조를 사용하여 내부 분기가 가능합니다.\n\n```typescript\nimport {\n  type CreateAgentSessionRuntimeFactory,\n  createAgentSession,\n  createAgentSessionFromServices,\n  createAgentSessionRuntime,\n  createAgentSessionServices,\n  getAgentDir,\n  SessionManager,\n} from \"@earendil-works/pi-coding-agent\";\n\n// In-memory (no persistence)\nconst { session } = await createAgentSession({\n  sessionManager: SessionManager.inMemory(),\n});\n\n// New persistent session\nconst { session: persisted } = await createAgentSession({\n  sessionManager: SessionManager.create(process.cwd()),\n});\n\n// Continue most recent\nconst { session: continued, modelFallbackMessage } = await createAgentSession({\n  sessionManager: SessionManager.continueRecent(process.cwd()),\n});\nif (modelFallbackMessage) {\n  console.log(\"Note:\", modelFallbackMessage);\n}\n\n// Open specific file\nconst { session: opened } = await createAgentSession({\n  sessionManager: SessionManager.open(\"/path/to/session.jsonl\"),\n});\n\n// List sessions\nconst currentProjectSessions = await SessionManager.list(process.cwd());\nconst allSessions = await SessionManager.listAll(process.cwd());\n\n// Session replacement API for /new, /resume, /fork, /clone, and import flows.\nconst createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {\n  const services = await createAgentSessionServices({ cwd });\n  return {\n    ...(await createAgentSessionFromServices({\n      services,\n      sessionManager,\n      sessionStartEvent,\n    })),\n    services,\n    diagnostics: services.diagnostics,\n  };\n};\n\nconst runtime = await createAgentSessionRuntime(createRuntime, {\n  cwd: process.cwd(),\n  agentDir: getAgentDir(),\n  sessionManager: SessionManager.create(process.cwd()),\n});\n\n// Replace the active session with a fresh one\nawait runtime.newSession();\n\n// Replace the active session with another saved session\nawait runtime.switchSession(\"/path/to/session.jsonl\");\n\n// Replace the active session with a fork from a specific user entry\nawait runtime.fork(\"entry-id\");\n\n// Clone the active path through a specific entry\nawait runtime.fork(\"entry-id\", { position: \"at\" });\n```\n\n**SessionManager 트리 API:**\n\n```typescript\nconst sm = SessionManager.open(\"/path/to/session.jsonl\");\n\n// Session listing\nconst currentProjectSessions = await SessionManager.list(process.cwd());\nconst allSessions = await SessionManager.listAll(process.cwd());\n\n// Tree traversal\nconst entries = sm.getEntries();        // All entries (excludes header)\nconst tree = sm.getTree();              // Full tree structure\nconst path = sm.getPath();              // Path from root to current leaf\nconst leaf = sm.getLeafEntry();         // Current leaf entry\nconst entry = sm.getEntry(id);          // Get entry by ID\nconst children = sm.getChildren(id);    // Direct children of entry\n\n// Labels\nconst label = sm.getLabel(id);          // Get label for entry\nsm.appendLabelChange(id, \"checkpoint\"); // Set label\n\n// Branching\nsm.branch(entryId);                     // Move leaf to earlier entry\nsm.branchWithSummary(id, \"Summary...\");  // Branch with context summary\nsm.createBranchedSession(leafId);       // Extract path to new file\n```\n\n> [examples/sdk/11-sessions.ts](../examples/sdk/11-sessions.ts) 및 [Session Format](session-format.md)를 참조하세요.\n\n### 설정 관리\n\n```typescript\nimport { createAgentSession, SettingsManager, SessionManager } from \"@earendil-works/pi-coding-agent\";\n\n// Default: loads from files (global + project merged)\nconst { session } = await createAgentSession({\n  settingsManager: SettingsManager.create(),\n});\n\n// With overrides\nconst settingsManager = SettingsManager.create();\nsettingsManager.applyOverrides({\n  compaction: { enabled: false },\n  retry: { enabled: true, maxRetries: 5 },\n});\nconst { session } = await createAgentSession({ settingsManager });\n\n// In-memory (no file I/O, for testing)\nconst { session } = await createAgentSession({\n  settingsManager: SettingsManager.inMemory({ compaction: { enabled: false } }),\n  sessionManager: SessionManager.inMemory(),\n});\n\n// Custom directories\nconst { session } = await createAgentSession({\n  settingsManager: SettingsManager.create(\"/custom/cwd\", \"/custom/agent\"),\n});\n```\n\n**정적 팩토리:**\n- `SettingsManager.create(cwd?, agentDir?)` - 파일에서 로드\n- `SettingsManager.inMemory(settings?)` - 파일 I/O 없음\n\n**프로젝트별 설정:**\n\n설정은 두 위치에서 로드되어 병합됩니다.\n1. 글로벌: `~/.pi/agent/settings.json`\n2. 프로젝트: `<cwd>/.pi/settings.json`\n\n프로젝트가 전역을 재정의합니다. 중첩된 객체는 키를 병합합니다. Setter는 기본적으로 전역 설정을 수정합니다.\n\n**지속성 및 오류 처리 의미:**\n\n- 설정 getter/setter는 메모리 내 상태에 대해 동기식입니다.\n- Setter는 대기열에 지속성을 비동기적으로 씁니다.\n- 내구성 경계가 필요한 경우(예: 프로세스 종료 전 또는 테스트에서 파일 내용을 어설션하기 전) `await settingsManager.flush()`를 호출하세요.\n- `SettingsManager`는 설정 I/O 오류를 인쇄하지 않습니다. `settingsManager.drainErrors()`를 사용하여 앱 레이어에 보고하세요.\n\n> [examples/sdk/10-settings.ts](../examples/sdk/10-settings.ts) 참조\n\n## 리소스로더\n\n확장 프로그램, 기술, 프롬프트, 테마 및 context files를 찾으려면 `DefaultResourceLoader`를 사용하세요.\n\n```typescript\nimport {\n  DefaultResourceLoader,\n  getAgentDir,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst loader = new DefaultResourceLoader({\n  cwd,\n  agentDir: getAgentDir(),\n});\nawait loader.reload();\n\nconst extensions = loader.getExtensions();\nconst skills = loader.getSkills();\nconst prompts = loader.getPrompts();\nconst themes = loader.getThemes();\nconst contextFiles = loader.getAgentsFiles().agentsFiles;\n```\n\n## 반환 값\n\n`createAgentSession()` 반환:\n\n```typescript\ninterface CreateAgentSessionResult {\n  // The session\n  session: AgentSession;\n  \n  // Extensions result (for runner setup)\n  extensionsResult: LoadExtensionsResult;\n  \n  // Warning if session model couldn't be restored\n  modelFallbackMessage?: string;\n}\n\ninterface LoadExtensionsResult {\n  extensions: Extension[];\n  errors: Array<{ path: string; error: string }>;\n  runtime: ExtensionRuntime;\n}\n```\n\n## 완전한 예\n\n```typescript\nimport { getModel } from \"@earendil-works/pi-ai\";\nimport { Type } from \"typebox\";\nimport {\n  createAgentSession,\n  DefaultResourceLoader,\n  defineTool,\n  ModelRuntime,\n  SessionManager,\n  SettingsManager,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst modelRuntime = await ModelRuntime.create({\n  authPath: \"/custom/agent/auth.json\",\n  modelsPath: \"/custom/agent/models.json\",\n});\nif (process.env.MY_KEY) {\n  await modelRuntime.setRuntimeApiKey(\"anthropic\", process.env.MY_KEY);\n}\n\n// Inline tool\nconst statusTool = defineTool({\n  name: \"status\",\n  label: \"Status\",\n  description: \"Get system status\",\n  parameters: Type.Object({}),\n  execute: async () => ({\n    content: [{ type: \"text\", text: `Uptime: ${process.uptime()}s` }],\n    details: {},\n  }),\n});\n\nconst model = getModel(\"anthropic\", \"claude-opus-4-5\");\nif (!model) throw new Error(\"Model not found\");\n\n// In-memory settings with overrides\nconst settingsManager = SettingsManager.inMemory({\n  compaction: { enabled: false },\n  retry: { enabled: true, maxRetries: 2 },\n});\n\nconst loader = new DefaultResourceLoader({\n  cwd: process.cwd(),\n  agentDir: \"/custom/agent\",\n  settingsManager,\n  systemPromptOverride: () => \"You are a minimal assistant. Be concise.\",\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({\n  cwd: process.cwd(),\n  agentDir: \"/custom/agent\",\n\n  model,\n  thinkingLevel: \"off\",\n  modelRuntime,\n\n  tools: [\"read\", \"bash\", \"status\"],\n  customTools: [statusTool],\n  resourceLoader: loader,\n\n  sessionManager: SessionManager.inMemory(),\n  settingsManager,\n});\n\nsession.subscribe((event) => {\n  if (event.type === \"message_update\" && event.assistantMessageEvent.type === \"text_delta\") {\n    process.stdout.write(event.assistantMessageEvent.delta);\n  }\n});\n\nawait session.prompt(\"Get status and list files.\");\n```\n\n## 실행 모드\n\nSDK는 `createAgentSession()` 위에 사용자 정의 인터페이스를 구축하기 위한 실행 모드 유틸리티를 내보냅니다.\n\n### 대화형 모드\n\n편집기, 채팅 기록 및 모든 내장 명령이 포함된 완전한 TUI 대화형 모드:\n\n```typescript\nimport {\n  type CreateAgentSessionRuntimeFactory,\n  createAgentSessionFromServices,\n  createAgentSessionRuntime,\n  createAgentSessionServices,\n  getAgentDir,\n  InteractiveMode,\n  SessionManager,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {\n  const services = await createAgentSessionServices({ cwd });\n  return {\n    ...(await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent })),\n    services,\n    diagnostics: services.diagnostics,\n  };\n};\nconst runtime = await createAgentSessionRuntime(createRuntime, {\n  cwd: process.cwd(),\n  agentDir: getAgentDir(),\n  sessionManager: SessionManager.create(process.cwd()),\n});\n\nconst mode = new InteractiveMode(runtime, {\n  migratedProviders: [],\n  modelFallbackMessage: undefined,\n  initialMessage: \"Hello\",\n  initialImages: [],\n  initialMessages: [],\n});\n\nawait mode.run();\n```\n\n### runPrintMode\n\n싱글샷 모드: 프롬프트 보내기, 결과 출력, 종료:\n\n```typescript\nimport {\n  type CreateAgentSessionRuntimeFactory,\n  createAgentSessionFromServices,\n  createAgentSessionRuntime,\n  createAgentSessionServices,\n  getAgentDir,\n  runPrintMode,\n  SessionManager,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {\n  const services = await createAgentSessionServices({ cwd });\n  return {\n    ...(await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent })),\n    services,\n    diagnostics: services.diagnostics,\n  };\n};\nconst runtime = await createAgentSessionRuntime(createRuntime, {\n  cwd: process.cwd(),\n  agentDir: getAgentDir(),\n  sessionManager: SessionManager.create(process.cwd()),\n});\n\nawait runPrintMode(runtime, {\n  mode: \"text\",\n  initialMessage: \"Hello\",\n  initialImages: [],\n  messages: [\"Follow up\"],\n});\n```\n\n### runRpc모드\n\n하위 프로세스 통합을 위한 JSON-RPC 모드:\n\n```typescript\nimport {\n  type CreateAgentSessionRuntimeFactory,\n  createAgentSessionFromServices,\n  createAgentSessionRuntime,\n  createAgentSessionServices,\n  getAgentDir,\n  runRpcMode,\n  SessionManager,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {\n  const services = await createAgentSessionServices({ cwd });\n  return {\n    ...(await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent })),\n    services,\n    diagnostics: services.diagnostics,\n  };\n};\nconst runtime = await createAgentSessionRuntime(createRuntime, {\n  cwd: process.cwd(),\n  agentDir: getAgentDir(),\n  sessionManager: SessionManager.create(process.cwd()),\n});\n\nawait runRpcMode(runtime);\n```\n\nJSON 프로토콜은 [RPC documentation](rpc.md)를 참조하세요.\n\n## RPC 모드 대안\n\nSDK를 사용하여 빌드하지 않고 하위 프로세스 기반 통합을 수행하려면 CLI를 직접 사용하세요.\n\n```bash\npi --mode rpc --no-session\n```\n\nJSON 프로토콜은 [RPC documentation](rpc.md)를 참조하세요.\n\nSDK는 다음과 같은 경우에 선호됩니다.\n- 유형 안전성을 원합니다\n- 당신도 같은 Node.js 과정을 밟고 있습니다\n- 에이전트 상태에 직접 액세스해야 합니다.\n- 도구/확장 프로그램을 프로그래밍 방식으로 사용자 정의하려는 경우\n\nRPC 모드는 다음과 같은 경우에 선호됩니다:\n- 다른 언어에서 통합하고 있습니다.\n- 프로세스 격리를 원합니다\n- 언어에 구애받지 않는 클라이언트를 구축하고 있습니다.\n\n## 수출\n\n주요 진입점은 다음을 내보냅니다.\n\n```typescript\n// Factory\ncreateAgentSession\ncreateAgentSessionRuntime\nAgentSessionRuntime\n\n// Auth and Models\nModelRuntime // implements pi-ai Models and owns credential storage\nModelRegistry // synchronous extension compatibility facade\nCredentialSynchronizationError\nresolveCliModel\nresolveModelScopeWithDiagnostics\n\n// Resource loading\nDefaultResourceLoader\ntype ResourceLoader\ncreateEventBus\n\n// Constants and helpers\nCONFIG_DIR_NAME\ndefineTool\ngetAgentDir\ngetPackageDir\ngetReadmePath\ngetDocsPath\ngetExamplesPath\n\n// Session management\nSessionManager\nSettingsManager\n\n// Tool factories\ncreateCodingTools\ncreateReadOnlyTools\ncreateReadTool, createBashTool, createEditTool, createWriteTool\ncreateGrepTool, createFindTool, createLsTool\n\n// Types\ntype CreateAgentSessionOptions\ntype CreateAgentSessionResult\ntype ExtensionFactory\ntype InlineExtension\ntype ExtensionAPI\ntype ToolDefinition\ntype Skill\ntype PromptTemplate\ntype Tool\n```\n\n확장 유형에 대해서는 [extensions.md](extensions.md) 전체 API를 참조하세요.","sourceFile":"sdk.md"},"security":{"title":"보안","markdown":"Pi는 로컬 코딩 에이전트입니다. 이를 시작하는 사용자 계정의 권한으로 실행되며 해당 사용자가 쓸 수 있는 파일을 동일한 로컬 신뢰 경계 내에 있는 것으로 처리합니다.\n\n## 프로젝트 신뢰\n\n프로젝트 신뢰는 pi가 프로젝트 로컬 설정, 리소스, 패키지 및 확장을 로드하는지 여부를 제어합니다. 이는 sandbox이 아니며 디렉토리에서 작업을 시작한 후 모델이 도구에 요청할 수 있는 작업을 제한하지 않습니다.\n\nPi 현재 작업 디렉터리에서 다음 중 하나를 찾으면 프로젝트에 신뢰가 필요한 리소스가 있는 것으로 간주합니다.\n\n- `.pi/settings.json`\n- `.pi/extensions`, `.pi/skills`, `.pi/prompts` 또는 `.pi/themes`\n- `.pi/SYSTEM.md` 또는 `.pi/APPEND_SYSTEM.md`\n- 현재 디렉터리 또는 상위 디렉터리의 프로젝트 `.agents/skills`\n\n`.pi` 디렉터리는 신뢰가 필요한 프로젝트 리소스로 간주되지 않습니다.\n\n신뢰가 필요하고 현재 디렉터리나 상위 디렉터리에 대해 저장된 결정이 없는 리소스가 있는 프로젝트에서 대화형 세션이 시작되면 pi는 전역 설정에서 `defaultProjectTrust`를 따릅니다. 기본값은 `\"ask\"`이며 UI를 사용할 수 있을 때 프로젝트를 신뢰할지 여부를 묻는 값입니다. 저장된 결정은 `~/.pi/agent/trust.json`의 표준 디렉터리에 저장되며, 현재 또는 상위 경로에 가장 가까운 저장된 결정이 전역 기본값 이전에 적용됩니다.\n\n프로젝트를 신뢰하면 pi는 다음을 포함하여 신뢰가 필요한 프로젝트 리소스를 로드할 수 있습니다.\n\n- `.pi/settings.json`\n- `.pi` 확장, 기술, prompt templates, 테마, 시스템 프롬프트 파일 등의 리소스\n- 프로젝트 설정을 통해 구성된 누락된 프로젝트 패키지\n- 프로젝트 로컬 확장 및 프로젝트 패키지 관리 확장\n\n신뢰가 감소하면 보호되는 리소스가 건너뜁니다. `AGENTS.override.md`, `AGENTS.md` 및 `CLAUDE.md`와 같은 컨텍스트 파일은 컨텍스트 로딩이 비활성화되지 않는 한 프로젝트 신뢰와 관계없이 로드됩니다. 신뢰가 해결되기 전에 pi는 context files, 사용자/전역 확장 및 CLI `-e` 확장만 로드합니다. 사용자/전역 및 CLI 확장은 `project_trust` 이벤트를 처리할 수 있습니다. 예/아니요 결정을 반환하는 첫 번째 확장이 결정을 소유합니다.\n\n비대화형 모드(`-p`, `--mode json` 및 `--mode rpc`)에는 신뢰 프롬프트가 표시되지 않습니다. 적용 가능한 저장된 신뢰 결정이 없으면 `defaultProjectTrust: \"ask\"` 및 `\"never\"`는 해당 리소스를 무시하고 `\"always\"`는 해당 리소스를 신뢰합니다. 한 번의 실행에 대해 프로젝트 신뢰를 재정의하려면 `--approve`/`-a` 또는 `--no-approve`/`-na`를 사용하세요.\n\n## 내장된 샌드박스 없음\n\nPi에는 내장된 sandbox가 포함되지 않습니다. 내장 도구는 pi 프로세스의 권한으로 파일을 읽고, 쓰고, 파일을 편집하고, 셸 명령을 실행할 수 있습니다. Extensions는 동일한 권한으로 실행되는 TypeScript 모듈입니다. 패키지 설치, 셸 명령, 언어 서버, 테스트 명령 및 기타 개발자 도구는 일반 로컬 프로세스처럼 작동합니다.\n\n이는 의도적인 것입니다. Pi은 로컬 소스 트리에서 작동하고, 프로젝트 도구 체인을 호출하고, 사용자의 기존 개발 환경과 통합되도록 설계되었습니다. 부분적인 진행 중인 sandbox은 호스트 셸, 파일 시스템, 패키지 관리자, 자격 증명 및 확장 코드에 계속 의존하면서 보안 경계로 오해되기 쉽습니다. 실제 격리는 운영 체제 또는 가상화/컨테이너 경계에서 이루어져야 합니다.\n\n프로젝트 신뢰는 입력 로딩 가드일 뿐입니다. 이는 승인하기 전에 저장소가 Pi의 설정이나 확장을 자동으로 변경하는 것을 방지합니다. 신뢰할 수 없는 코드, 신뢰할 수 없는 프롬프트 또는 신뢰할 수 없는 모델 출력을 안전하게 만들지는 않습니다. 저장소 파일, 주석, 문서, context files 또는 빌드 출력에서 ​​즉각적인 삽입은 로컬 에이전트 위험이 예상되며 pi로 안정적으로 방지할 수 없습니다.\n\n## 신뢰할 수 없거나 모니터링되지 않는 작업 실행\n\n신뢰할 수 없는 리포지토리, 면밀히 모니터링할 의도가 없는 생성된 코드 또는 무인 자동화의 경우 포함된 환경에서 pi를 실행합니다. 작업에 필요한 파일과 자격 증명만 포함하여 컨테이너, VM, 마이크로 VM, 원격 sandbox 또는 정책 제어 sandbox를 사용하세요.\n\n일반적인 패턴은 [Containerization](containerization.md)에 설명되어 있습니다.\n\n- 전체 `pi` 프로세스를 컨테이너/sandbox 내부에서 실행합니다.\n- 내장 도구 실행을 Gondolin 마이크로 VM으로 라우팅하는 동안 호스트 pi를 실행합니다.\n- 에이전트가 액세스해야 하는 작업공간 경로만 마운트\n- 컨테이너가 호스트 세션, 설정 및 자격 증명에 액세스해야 하지 않는 한 호스트 마운트를 피하세요 `~/.pi/agent`\n- 최소 필수 API keys를 통과하거나 단기 사용자 인증 정보를 사용하세요.\n- 작업에 필요하지 않을 때 네트워크 액세스를 제한합니다.\n- 결과를 신뢰할 수 있는 시스템에 다시 복사하기 전에 차이점과 출력을 검토하세요.\n\n호스트 작업 영역 읽기/쓰기를 바인드 탑재하는 경우 컨테이너 또는 VM 내부의 쓰기로 인해 호스트 파일이 계속 수정될 수 있습니다. 의도하지 않은 쓰기로부터 더 강력한 보호가 필요한 경우 읽기 전용 마운트를 사용하거나 sandbox 안팎으로 파일을 복사하세요.\n\n## 보안 문제 보고\n\n보안 문제를 보고하려면 저장소 [Security Policy](https://github.com/earendil-works/pi-mono/blob/main/SECURITY.md)를 팔로우하세요. 보안에 민감한 보고서에 대해서는 공개 문제를 공개하지 마십시오.\n\n예상되는 로컬 에이전트 동작, 내장된 sandbox 부족, 신뢰할 수 없는 콘텐츠의 신속한 삽입, 사용자가 설치한 확장 프로그램이나 기술의 동작은 보고서에서 실제 권한 경계 우회를 보여주거나 pi가 로컬 사용자에게 아직 없는 액세스 권한을 부여하는 방법을 보여주지 않는 한 일반적으로 보안 경계 외부에 있습니다.","sourceFile":"security.md"},"session-format":{"title":"세션 파일 형식","markdown":"세션은 JSONL(JSON Lines) 파일로 저장됩니다. 각 줄은 `type` 필드가 있는 JSON 객체입니다. 세션 항목은 `id`/`parentId` 필드를 통해 트리 구조를 형성하므로 새 파일을 만들지 않고도 내부 분기가 가능합니다.\n\n## 파일 위치\n\n```\n~/.pi/agent/sessions/--<path>--/<timestamp>_<uuid>.jsonl\n```\n\n`<path>`는 `/`가 `-`로 대체된 작업 디렉토리입니다.\n\n## 세션 삭제\n\n`~/.pi/agent/sessions/` 아래의 `.jsonl` 파일을 삭제하면 세션을 제거할 수 있습니다.\n\nPi는 `/resume`에서 대화형으로 세션을 삭제할 수도 있습니다(세션을 선택하고 `Ctrl+D`를 누른 다음 확인). 가능한 경우 pi는 `trash` CLI를 사용하여 영구 삭제를 방지합니다.\n\n## 세션 버전\n\n세션 헤더에는 버전 필드가 있습니다.\n\n- **버전 1**: 선형 항목 순서(레거시, 로드 시 자동 마이그레이션)\n- **버전 2**: `id`/`parentId` 연결이 포함된 트리 구조\n- **버전 3**: `hookMessage` 역할 이름을 `custom`로 변경(확장 프로그램 통합)\n\n기존 세션은 로드 시 자동으로 현재 버전(v3)으로 마이그레이션됩니다.\n\n## 소스 파일\n\nGitHub([pi-mono](https://github.com/earendil-works/pi-mono))의 출처:\n- [`packages/coding-agent/src/core/session-manager.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/session-manager.ts) - 세션 항목 유형 및 SessionManager\n- [`packages/coding-agent/src/core/messages.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/messages.ts) - 확장된 메시지 유형(BashExecutionMessage, CustomMessage 등)\n- [`packages/ai/src/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/types.ts) - 기본 메시지 유형(UserMessage, AssistantMessage, ToolResultMessage)\n- [`packages/agent/src/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/agent/src/types.ts) - AgentMessage 통합 유형\n\n프로젝트의 TypeScript 정의에 대해서는 `node_modules/@earendil-works/pi-coding-agent/dist/` 및 `node_modules/@earendil-works/pi-ai/dist/`를 검사하세요.\n\n## 메시지 유형\n\n세션 항목에는 `AgentMessage` 개체가 포함되어 있습니다. 세션을 구문 분석하고 확장을 작성하려면 이러한 유형을 이해하는 것이 필수적입니다.\n\n### 콘텐츠 블록\n\n메시지에는 입력된 콘텐츠 블록의 배열이 포함됩니다.\n\n```typescript\ninterface TextContent {\n  type: \"text\";\n  text: string;\n}\n\ninterface ImageContent {\n  type: \"image\";\n  data: string;      // base64 encoded\n  mimeType: string;  // e.g., \"image/jpeg\", \"image/png\"\n}\n\ninterface ThinkingContent {\n  type: \"thinking\";\n  thinking: string;\n}\n\ninterface ToolCall {\n  type: \"toolCall\";\n  id: string;\n  name: string;\n  arguments: Record<string, any>;\n}\n```\n\n### 기본 메시지 유형(pi-ai에서)\n\n```typescript\ninterface UserMessage {\n  role: \"user\";\n  content: string | (TextContent | ImageContent)[];\n  timestamp: number;  // Unix ms\n}\n\ninterface AssistantMessage {\n  role: \"assistant\";\n  content: (TextContent | ThinkingContent | ToolCall)[];\n  api: string;\n  provider: string;\n  model: string;\n  usage: Usage;\n  stopReason: \"stop\" | \"length\" | \"toolUse\" | \"error\" | \"aborted\";\n  errorMessage?: string;\n  timestamp: number;\n}\n\ninterface ToolResultMessage {\n  role: \"toolResult\";\n  toolCallId: string;\n  toolName: string;\n  content: (TextContent | ImageContent)[];\n  details?: any;      // Tool-specific metadata\n  usage?: Usage;      // Nested LLM work performed by the tool\n  isError: boolean;\n  timestamp: number;\n}\n\ninterface Usage {\n  input: number;\n  output: number;\n  cacheRead: number;\n  cacheWrite: number;\n  totalTokens: number;\n  cost: {\n    input: number;\n    output: number;\n    cacheRead: number;\n    cacheWrite: number;\n    total: number;\n  };\n}\n```\n\n내보낸 pi-ai `StopReason` 유형에는 `\"pending\"`도 포함되지만 해당 값은 스트리밍 이벤트의 부분 메시지용으로 예약되어 있습니다. 터미널 `done`/`error` 메시지는 pi가 보조 메시지를 지속하기 전에 이를 완료 이유로 대체하므로 `\"pending\"`는 세션 JSONL에 절대 나타나서는 안 됩니다.\n\n### 확장 메시지 유형(pi-coding-agent에서)\n\n```typescript\ninterface BashExecutionMessage {\n  role: \"bashExecution\";\n  command: string;\n  output: string;\n  exitCode: number | undefined;\n  cancelled: boolean;\n  truncated: boolean;\n  fullOutputPath?: string;\n  excludeFromContext?: boolean;  // true for !! prefix commands\n  timestamp: number;\n}\n\ninterface CustomMessage {\n  role: \"custom\";\n  customType: string;            // Extension identifier\n  content: string | (TextContent | ImageContent)[];\n  display: boolean;              // Show in TUI\n  details?: any;                 // Extension-specific metadata\n  timestamp: number;\n}\n\ninterface BranchSummaryMessage {\n  role: \"branchSummary\";\n  summary: string;\n  fromId: string;                // Entry we branched from\n  timestamp: number;\n}\n\ninterface CompactionSummaryMessage {\n  role: \"compactionSummary\";\n  summary: string;\n  tokensBefore: number;\n  timestamp: number;\n}\n```\n\n### AgentMessage Union\n\n```typescript\ntype AgentMessage =\n  | UserMessage\n  | AssistantMessage\n  | ToolResultMessage\n  | BashExecutionMessage\n  | CustomMessage\n  | BranchSummaryMessage\n  | CompactionSummaryMessage;\n```\n\n## 진입 기지\n\n모든 항목(`SessionHeader` 제외)은 `SessionEntryBase`을 확장합니다.\n\n```typescript\ninterface SessionEntryBase {\n  type: string;\n  id: string;           // 8-char hex ID\n  parentId: string | null;  // Parent entry ID (null for first entry)\n  timestamp: string;    // ISO timestamp\n}\n```\n\n## 항목 유형\n\n### 세션헤더\n\n파일의 첫 번째 줄. 메타데이터만 해당되며 트리의 일부는 아닙니다(`id`/`parentId` 없음).\n\n```json\n{\"type\":\"session\",\"version\":3,\"id\":\"uuid\",\"timestamp\":\"2024-12-03T14:00:00.000Z\",\"cwd\":\"/path/to/project\"}\n```\n\n부모와 함께하는 세션의 경우(`/fork`, `/clone` 또는 `newSession({ parentSession })`를 통해 생성됨):\n\n```json\n{\"type\":\"session\",\"version\":3,\"id\":\"uuid\",\"timestamp\":\"2024-12-03T14:00:00.000Z\",\"cwd\":\"/path/to/project\",\"parentSession\":\"/path/to/original/session.jsonl\"}\n```\n\n### 세션메시지항목\n\n대화의 메시지입니다. `message` 필드에는 `AgentMessage`이 포함되어 있습니다.\n\n```json\n{\"type\":\"message\",\"id\":\"a1b2c3d4\",\"parentId\":\"prev1234\",\"timestamp\":\"2024-12-03T14:00:01.000Z\",\"message\":{\"role\":\"user\",\"content\":\"Hello\"}}\n{\"type\":\"message\",\"id\":\"b2c3d4e5\",\"parentId\":\"a1b2c3d4\",\"timestamp\":\"2024-12-03T14:00:02.000Z\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"Hi!\"}],\"provider\":\"anthropic\",\"model\":\"claude-sonnet-4-5\",\"usage\":{...},\"stopReason\":\"stop\"}}\n{\"type\":\"message\",\"id\":\"c3d4e5f6\",\"parentId\":\"b2c3d4e5\",\"timestamp\":\"2024-12-03T14:00:03.000Z\",\"message\":{\"role\":\"toolResult\",\"toolCallId\":\"call_123\",\"toolName\":\"bash\",\"content\":[{\"type\":\"text\",\"text\":\"output\"}],\"isError\":false}}\n```\n\n### 모델변경항목\n\n사용자가 세션 중에 모델을 전환할 때 발생합니다.\n\n```json\n{\"type\":\"model_change\",\"id\":\"d4e5f6g7\",\"parentId\":\"c3d4e5f6\",\"timestamp\":\"2024-12-03T14:05:00.000Z\",\"provider\":\"openai\",\"modelId\":\"gpt-4o\"}\n```\n\n### 사고수준변경항목\n\n사용자가 사고/추리 수준을 변경할 때 발생합니다.\n\n```json\n{\"type\":\"thinking_level_change\",\"id\":\"e5f6g7h8\",\"parentId\":\"d4e5f6g7\",\"timestamp\":\"2024-12-03T14:06:00.000Z\",\"thinkingLevel\":\"high\"}\n```\n\n### 압축항목\n\n컨텍스트가 압축될 때 생성됩니다. 이전 메시지의 요약을 저장합니다.\n\n```json\n{\"type\":\"compaction\",\"id\":\"f6g7h8i9\",\"parentId\":\"e5f6g7h8\",\"timestamp\":\"2024-12-03T14:10:00.000Z\",\"summary\":\"User discussed X, Y, Z...\",\"firstKeptEntryId\":\"c3d4e5f6\",\"tokensBefore\":50000}\n```\n\n최신 하네스 생성 압축은 `firstKeptEntryId` 대신 유지된 압축 후 컨텍스트를 항목에 직접 포함합니다.\n\n```json\n{\"type\":\"compaction\",\"id\":\"f6g7h8i9\",\"parentId\":\"e5f6g7h8\",\"timestamp\":\"2024-12-03T14:10:00.000Z\",\"summary\":\"User discussed X, Y, Z...\",\"tokensBefore\":50000,\"retainedTail\":[{\"role\":\"user\",\"content\":\"latest request\"},{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"latest reply\"}],\"provider\":\"anthropic\",\"model\":\"claude-sonnet-4-5\",\"usage\":{...},\"stopReason\":\"stop\"}]}\n```\n\n선택 필드:\n- `usage`: 요약 생성에서 LLM 사용; 세션 토큰 및 총 비용에 포함됨\n- `retainedTail`: 구체화됨 `AgentMessage[]` 압축 후 유지됩니다. 이는 이전 세션과의 호환성을 위해서만 선택 사항입니다. 최신 하네스 생성 압축에는 이를 포함하므로 압축 항목 이전에 이전 항목을 탐색하지 않고도 이 체크포인트에서 컨텍스트를 다시 빌드할 수 있습니다.\n- `details`: 구현별 데이터(예: 기본값의 경우 `{ readFiles: string[], modifiedFiles: string[] }`, 확장의 경우 맞춤 데이터)\n- `fromHook`: `true` 확장 프로그램에 의해 생성된 경우, `false`/`undefined` pi로 생성된 경우(레거시 필드 이름)\n- `firstKeptEntryId`: 이전 항목 형식과의 호환성을 위해.\n\n### 분기요약 항목\n\n공통 조상까지 왼쪽 분기의 LLM 생성 요약을 사용하여 `/tree`를 통해 분기를 전환할 때 생성됩니다. 버려진 경로에서 컨텍스트를 캡처합니다.\n\n```json\n{\"type\":\"branch_summary\",\"id\":\"g7h8i9j0\",\"parentId\":\"a1b2c3d4\",\"timestamp\":\"2024-12-03T14:15:00.000Z\",\"fromId\":\"f6g7h8i9\",\"summary\":\"Branch explored approach A...\"}\n```\n\n선택 필드:\n- `usage`: 요약 생성에서 LLM 사용; 세션 토큰 및 총 비용에 포함됨\n- `details`: 기본 파일 추적 데이터(`{ readFiles: string[], modifiedFiles: string[] }`) 또는 확장 프로그램의 맞춤 데이터\n- `fromHook`: `true` 확장 프로그램에 의해 생성된 경우, `false`/`undefined` pi로 생성된 경우(레거시 필드 이름)\n\n### 맞춤 항목\n\n확장 상태 지속성. LLM 컨텍스트에 참여하지 않습니다.\n\n```json\n{\"type\":\"custom\",\"id\":\"h8i9j0k1\",\"parentId\":\"g7h8i9j0\",\"timestamp\":\"2024-12-03T14:20:00.000Z\",\"customType\":\"my-extension\",\"data\":{\"count\":42}}\n```\n\n다시 로드할 때 확장 프로그램 항목을 식별하려면 `customType`를 사용하세요. 대화형 모드는 `pi.registerEntryRenderer(customType, renderer)`를 통해 사용자 정의 항목을 렌더링할 수 있지만 여전히 LLM 컨텍스트에 참여하지 않습니다.\n\n### 사용자 정의 메시지 항목\n\nLLM 컨텍스트에 참여하는 확장 프로그램 삽입 메시지입니다.\n\n```json\n{\"type\":\"custom_message\",\"id\":\"i9j0k1l2\",\"parentId\":\"h8i9j0k1\",\"timestamp\":\"2024-12-03T14:25:00.000Z\",\"customType\":\"my-extension\",\"content\":\"Injected context...\",\"display\":true}\n```\n\n전지:\n- `content`: 문자열 또는 `(TextContent | ImageContent)[]` (UserMessage와 동일)\n- `display`: `true` = 독특한 스타일로 TUI에 표시, `false` = 숨김\n- `details`: 선택적 확장 관련 메타데이터(LLM으로 전송되지 않음)\n\n### 라벨 항목\n\n항목에 대한 사용자 정의 북마크/마커입니다.\n\n```json\n{\"type\":\"label\",\"id\":\"j0k1l2m3\",\"parentId\":\"i9j0k1l2\",\"timestamp\":\"2024-12-03T14:30:00.000Z\",\"targetId\":\"a1b2c3d4\",\"label\":\"checkpoint-1\"}\n```\n\n라벨을 지우려면 `label`에서 `undefined`로 설정하세요.\n\n### 세션정보항목\n\n세션 메타데이터(예: 사용자 정의 표시 이름) 확장 프로그램에서 `/name`, `--name` / `-n` 또는 `pi.setSessionName()`를 통해 설정하세요.\n\n```json\n{\"type\":\"session_info\",\"id\":\"k1l2m3n4\",\"parentId\":\"j0k1l2m3\",\"timestamp\":\"2024-12-03T14:35:00.000Z\",\"name\":\"Refactor auth module\"}\n```\n\n세션 이름은 설정 시 첫 번째 메시지 대신 세션 선택기(`/resume`)에 표시됩니다.\n\n## 트리 구조\n\n항목은 트리를 형성합니다.\n- 첫 번째 항목은 `parentId: null`입니다.\n- 각 후속 항목은 `parentId`를 통해 상위 항목을 가리킵니다.\n- 분기는 이전 항목에서 새 하위 항목을 만듭니다.\n- \"리프\"는 트리의 현재 위치입니다.\n\n```\n[user msg] ─── [assistant] ─── [user msg] ─── [assistant] ─┬─ [user msg] ← current leaf\n                                                            │\n                                                            └─ [branch_summary] ─── [user msg] ← alternate branch\n```\n\n## 컨텍스트 구축\n\n`buildContextEntries()` 현재 리프에서 루트로 이동하여 압축을 유지하면서 활성 항목 목록을 생성합니다.\n\n1. 경로의 모든 항목을 수집합니다.\n2. `CompactionEntry`가 경로에 있는 경우:\n   - 먼저 압축 항목을 포함합니다.\n   - `retainedTail`가 있는 경우 독립형 체크포인트 역할을 하며 압축 후 항목이 포함됩니다.\n   - 그렇지 않으면 `firstKeptEntryId`부터 압축까지의 항목이 포함됩니다.\n   - 그런 다음 압축 후 항목이 포함됩니다.\n3. 대화형 모드에서 렌더링할 수 있도록 선택한 범위의 메시지가 아닌 항목을 보존합니다.\n\n`buildSessionContext()` 해당 항목 목록을 기반으로 LLM에 대한 메시지 목록을 생성합니다.\n\n1. 전체 경로에서 현재 모델 및 사고 수준 설정을 추출합니다.\n2. 선택한 항목을 메시지로 변환합니다.\n   - `message` -> 저장됨 `AgentMessage`\n   - `compaction` -> `compactionSummary` + `retainedTail` 존재하는 경우\n   - `branch_summary` -> `branchSummary`\n   - `custom_message` -> `CustomMessage`\n   - `custom` -> 컨텍스트 메시지 없음\n\n이로 인해 최신 압축이 독립적인 체크포인트처럼 작동합니다. `retainedTail`는 선택 사항이므로 `firstKeptEntryId`만 저장하는 이전 세션이 계속해서 올바르게 로드됩니다.\n\n## 파싱 ​​예\n\n```typescript\nimport { readFileSync } from \"fs\";\n\nconst lines = readFileSync(\"session.jsonl\", \"utf8\").trim().split(\"\\n\");\n\nfor (const line of lines) {\n  const entry = JSON.parse(line);\n\n  switch (entry.type) {\n    case \"session\":\n      console.log(`Session v${entry.version ?? 1}: ${entry.id}`);\n      break;\n    case \"message\":\n      console.log(`[${entry.id}] ${entry.message.role}: ${JSON.stringify(entry.message.content)}`);\n      break;\n    case \"compaction\":\n      console.log(`[${entry.id}] Compaction: ${entry.tokensBefore} tokens summarized`);\n      break;\n    case \"branch_summary\":\n      console.log(`[${entry.id}] Branch from ${entry.fromId}`);\n      break;\n    case \"custom\":\n      console.log(`[${entry.id}] Custom (${entry.customType}): ${JSON.stringify(entry.data)}`);\n      break;\n    case \"custom_message\":\n      console.log(`[${entry.id}] Extension message (${entry.customType}): ${entry.content}`);\n      break;\n    case \"label\":\n      console.log(`[${entry.id}] Label \"${entry.label}\" on ${entry.targetId}`);\n      break;\n    case \"model_change\":\n      console.log(`[${entry.id}] Model: ${entry.provider}/${entry.modelId}`);\n      break;\n    case \"thinking_level_change\":\n      console.log(`[${entry.id}] Thinking: ${entry.thinkingLevel}`);\n      break;\n  }\n}\n```\n\n## 세션 관리자 API\n\n프로그래밍 방식으로 세션을 작업하기 위한 주요 방법입니다.\n\n### 정적 생성 방법\n- `SessionManager.create(cwd, sessionDir?)` - 새 세션\n- `SessionManager.open(path, sessionDir?)` - 기존 세션 파일 열기\n- `SessionManager.continueRecent(cwd, sessionDir?)` - 최신 항목을 계속 진행하거나 새로 만들기\n- `SessionManager.inMemory(cwd?)` - 파일 지속성 없음\n- `SessionManager.forkFrom(sourcePath, targetCwd, sessionDir?)` - 다른 프로젝트의 포크 세션\n\n### 정적 상장 방법\n- `SessionManager.list(cwd, sessionDir?, onProgress?)` - 디렉토리의 세션 나열\n- `SessionManager.listAll(onProgress?)` - 모든 프로젝트의 모든 세션 나열\n\n### 인스턴스 메소드 - 세션 관리\n- `newSession(options?)` - 새 세션 시작(옵션: `{ parentSession?: string }`)\n- `setSessionFile(path)` - 다른 세션 파일로 전환\n- `createBranchedSession(leafId)` - 새 세션 파일로 분기 추출\n\n### 인스턴스 메서드 - 추가(모든 반품 항목 ID)\n- `appendMessage(message)` - 메시지 추가\n- `appendThinkingLevelChange(level)` - 생각의 변화를 기록하세요\n- `appendModelChange(provider, modelId)` - 모델 변경 기록\n- `appendCompaction(summary, firstKeptEntryId, tokensBefore, details?, fromHook?)` - 압축 추가\n- `appendCustomEntry(customType, data?)` - 확장 상태(컨텍스트가 아님)\n- `appendSessionInfo(name)` - 세션 표시 이름 설정\n- `appendCustomMessageEntry(customType, content, display, details?)` - 확장 메시지(컨텍스트)\n- `appendLabelChange(targetId, label)` - 라벨 설정/삭제\n\n### 인스턴스 메서드 - 트리 탐색\n- `getLeafId()` - 현재 위치\n- `getLeafEntry()` - 현재 리프 항목 가져오기\n- `getEntry(id)` - ID로 항목 받기\n- `getBranch(fromId?)` - 입구에서 뿌리까지 걷기\n- `getTree()` - 전체 트리 구조 얻기\n- `getChildren(parentId)` - 자녀를 직접 만나보세요\n- `getLabel(id)` - 입장 라벨 받기\n- `branch(entryId)` - 리프를 이전 항목으로 이동\n- `resetLeaf()` - 리프를 null로 재설정(항목 전)\n- `branchWithSummary(entryId, summary, details?, fromHook?)` - 컨텍스트 요약이 포함된 분기\n\n### 인스턴스 메소드 - 컨텍스트 및 정보\n- `buildContextEntries()` - 압축이 적용된 활성 분기 항목 가져오기\n- `buildSessionContext()` - LLM을 위한 메시지, ThinkingLevel 및 모델 가져오기\n- `getEntries()` - 모든 항목(헤더 제외)\n- `getHeader()` - 세션 헤더 메타데이터\n- `getSessionName()` - 최신 session_info 항목에서 표시 이름 가져오기\n- `getCwd()` - 작업 디렉토리\n- `getSessionDir()` - 세션 저장 디렉터리\n- `getSessionId()` - 세션 UUID\n- `getSessionFile()` - 세션 파일 경로(인메모리의 경우 정의되지 않음)\n- `isPersisted()` - 세션이 디스크에 저장되는지 여부","sourceFile":"session-format.md"},"sessions":{"title":"세션","markdown":"Pi 대화를 세션으로 저장하여 작업을 계속하고, 이전 단계에서 분기하고, 이전 경로를 다시 방문할 수 있습니다.\n\n## 세션 저장\n\n세션은 작업 디렉터리별로 구성되어 `~/.pi/agent/sessions/`에 자동 저장됩니다. 각 세션은 트리 구조의 JSONL 파일입니다.\n\n```bash\npi -c                  # Continue most recent session\npi -r                  # Browse and select from past sessions\npi --no-session        # Ephemeral mode; do not save\npi --name \"my task\"    # Set session display name at startup\npi --session <path|id> # Use a specific session file or partial session ID\npi --fork <path|id>    # Fork a session file or partial session ID into a new session\n```\n\n현재 세션 파일, 세션 ID, 메시지 수, 토큰 및 비용을 보려면 대화형 모드에서 `/session`를 사용하세요.\n\nJSONL 파일 형식과 SessionManager API에 대해서는 [Session Format](session-format.md)를 참조하세요.\n\n## 세션 명령\n\n| 명령 | 설명 |\n|---------|-------------|\n| `/resume` | 이전 세션 찾아보기 및 선택 |\n| `/new` | 새 세션 시작 |\n| `/name <name>` | 현재 세션 표시 이름 설정 |\n| `/session` | 세션 정보 표시 |\n| `/tree` | 현재 탐색 session tree |\n| `/fork` | 이전 사용자 메시지에서 새 세션 만들기 |\n| `/clone` | 현재 활성 분기를 새 세션에 복제합니다. |\n| `/compact [prompt]` | 이전 맥락을 요약합니다. [Compaction](compaction.md) 참조 |\n| `/export [file]` | 세션을 HTML로 내보내기 |\n| `/share` | 공유 가능한 HTML 링크가 포함된 비공개 GitHub 요점으로 업로드 |\n\n## 세션 재개 및 삭제\n\n`/resume` 현재 프로젝트에 대한 대화형 세션 선택기를 엽니다. `pi -r` 시작 시 동일한 선택기를 엽니다.\n\n선택기에서 다음을 수행할 수 있습니다.\n\n- 입력하여 검색\n- Ctrl+P를 사용하여 경로 표시 전환\n- Ctrl+S를 사용하여 정렬 모드 전환\n- Ctrl+N을 사용하여 명명된 세션으로 필터링\n- Ctrl+R로 이름 바꾸기\n- Ctrl+D로 삭제한 후 확인\n\n가능한 경우 pi는 파일을 영구적으로 제거하는 대신 삭제를 위해 `trash` CLI를 사용합니다.\n\n## 세션 이름 지정\n\n사람이 읽을 수 있는 세션 이름을 설정하려면 `/name <name>`를 사용하세요.\n\n```text\n/name Refactor auth module\n```\n\n`--name` 또는 `-n`를 사용하여 시작 시 이름을 설정합니다.\n\n```bash\npi --name \"Refactor auth module\"\npi --name \"CI audit\" -p \"Review this build failure\"\n```\n\n명명된 세션은 `/resume` 및 `pi -r`에서 더 쉽게 찾을 수 있습니다.\n\n## `/tree`로 분기\n\n세션은 트리로 저장됩니다. 모든 항목에는 `id` 및 `parentId`가 있으며 현재 위치는 활성 리프입니다. `/tree`를 사용하면 새 파일을 만들지 않고도 이전 지점으로 이동하여 계속할 수 있습니다.\n\n<p align=\"center\"><img src=\"images/tree-view.png\" alt=\"Tree View\" width=\"600\"></p>\n\n예시 모양:\n\n```text\n├─ user: \"Hello, can you help...\"\n│  └─ assistant: \"Of course! I can...\"\n│     ├─ user: \"Let's try approach A...\"\n│     │  └─ assistant: \"For approach A...\"\n│     │     └─ user: \"That worked...\"  ← active\n│     └─ user: \"Actually, approach B...\"\n│        └─ assistant: \"For approach B...\"\n```\n\n### 트리 컨트롤\n\n| 열쇠 | 행동 |\n|-----|--------|\n| ↑/↓ | 보이는 항목 탐색 |\n| ←/→ | 페이지 위로/아래로 |\n| Ctrl+←/Ctrl+→ 또는 Alt+←/Alt+→ | 접기/펼치기 또는 분기 세그먼트 간 점프 |\n| Shift+L | 선택한 항목에 대한 레이블 설정 또는 지우기 |\n| Shift+T | 라벨 타임스탬프 전환 |\n| 입력하다 | 항목 선택 |\n| 이스케이프/Ctrl+C | 취소 |\n| Ctrl+O | 사이클 필터 모드 |\n\n필터 모드에는 기본값, 도구 없음, 사용자 전용, 레이블만 지정 및 모두가 있습니다. [Settings](settings.md)에서 `treeFilterMode`로 기본값을 구성합니다.\n\n### 선택 동작\n\n사용자 또는 사용자 정의 메시지 선택:\n\n1. 리프를 선택한 메시지의 상위 항목으로 이동합니다.\n2. 선택한 메시지 텍스트를 편집기에 배치합니다.\n3. 편집하고 다시 제출하여 새 분기를 만들 수 있습니다.\n\n보조자, 도구, 압축 또는 기타 비사용자 항목 선택:\n\n1. 리프를 해당 항목으로 이동합니다.\n2. 편집기를 비워 둡니다.\n3. 그 시점부터 계속하겠습니다.\n\n루트 사용자 메시지를 선택하면 리프가 빈 대화로 재설정되고 원래 프롬프트가 편집기에 배치됩니다.\n\n## `/tree`, `/fork`, `/clone`\n\n| 특징 | `/tree` | `/fork` | `/clone` |\n|---------|---------|---------|----------|\n| 산출 | 동일한 세션 파일 | 새 세션 파일 | 새 세션 파일 |\n| 보다 | 풀 트리 | 사용자 메시지 선택기 | 현재 활성 분기 |\n| 일반적인 사용 | 현재 대안 탐색 | 이전 프롬프트에서 새 세션 시작 | 계속하기 전에 현재 작업을 복제하세요. |\n| 요약 | 선택적 분기 요약 | 없음 | 없음 |\n\n대안을 함께 유지하려면 `/tree`를 사용하세요. 별도의 세션 파일을 원할 경우 `/fork` 또는 `/clone`를 사용하세요.\n\n## 지점 요약\n\n`/tree`가 한 분기에서 다른 분기로 전환되면 pi는 버려진 분기를 요약하고 해당 요약을 새 위치에 첨부할 수 있습니다. 이렇게 하면 전체 분기를 재생하지 않고 떠난 경로의 중요한 컨텍스트가 보존됩니다.\n\n메시지가 표시되면 다음 중 하나를 선택합니다.\n\n1. 요약 없음\n2. 기본 프롬프트로 요약\n3. 사용자 정의 초점 지침으로 요약\n\nbranch summarization 내부 및 확장 후크에 대해서는 [Compaction](compaction.md)를 참조하세요.\n\n## 세션 형식\n\n세션 파일은 JSONL이며 메시지 항목, 모델 변경, 사고 수준 변경, 레이블, 압축, 분기 요약 및 확장 항목을 포함합니다.\n\n파서, 확장, SDK 사용법 및 전체 SessionManager API에 대해서는 [Session Format](session-format.md)를 참조하세요.","sourceFile":"sessions.md"},"settings":{"title":"설정","markdown":"Pi는 전역 설정을 재정의하는 프로젝트 설정과 함께 JSON 설정 파일을 사용합니다.\n\n| 위치 | 범위 |\n|----------|-------|\n| `~/.pi/agent/settings.json` | 글로벌(모든 프로젝트) |\n| `.pi/settings.json` | 프로젝트(현재 디렉터리) |\n\n직접 편집하거나 일반 옵션에 `/settings`를 사용하세요.\n\n## 프로젝트 신뢰\n\n대화형 시작 시 pi는 프로젝트 로컬 설정, 리소스 또는 프로젝트 `.agents/skills`를 포함하고 `~/.pi/agent/trust.json`의 폴더 또는 상위 폴더에 대해 저장된 결정이 없는 프로젝트 폴더를 신뢰하기 전에 묻습니다. 프로젝트를 신뢰하면 pi가 `.pi/settings.json` 및 `.pi` 리소스를 로드하고, 누락된 프로젝트 패키지를 설치하고, 프로젝트 확장을 실행할 수 있습니다.\n\n비대화형 모드(`-p`, `--mode json` 및 `--mode rpc`)에는 신뢰 프롬프트가 표시되지 않습니다. 적용 가능한 저장된 신뢰 결정이 없으면 전역 설정에서 `defaultProjectTrust`를 사용합니다. `ask`(기본값) 및 `never`는 해당 프로젝트 리소스를 무시하고 `always`는 이를 신뢰합니다. 한 번의 실행에 대해 프로젝트 신뢰를 재정의하려면 `--approve`/`-a` 또는 `--no-approve`/`-na`를 전달합니다.\n\n확장이나 저장된 결정이 적용되지 않는 경우 `defaultProjectTrust`는 대체 동작을 제어합니다. `~/.pi/agent/settings.json`에서는 `\"ask\"`, `\"always\"`, `\"never\"`로 설정하거나 `/settings`로 변경하세요.\n\n`pi config` 및 패키지 명령은 동일한 프로젝트 신뢰 흐름을 사용합니다. 단, `pi update`는 메시지를 표시하지 않습니다. 하나의 명령에 대해 프로젝트 로컬 설정을 신뢰하려면 `--approve`를 전달하고 이를 무시하려면 `--no-approve`를 전달합니다.\n\n직계 상위 폴더에 대한 신뢰를 포함하여 향후 세션에 대한 프로젝트 신뢰 결정을 저장하려면 대화형 모드에서 `/trust`를 사용하세요. `~/.pi/agent/trust.json`만 씁니다. 현재 세션은 다시 로드되지 않으므로 변경 사항을 적용하려면 pi를 다시 시작하세요.\n\n## 모든 설정\n\n### 모델과 사고\n\n| 환경 | 유형 | 기본 | 설명 |\n|---------|------|---------|-------------|\n| `defaultProvider` | 끈 | - | 기본 공급자(예: `\"anthropic\"`, `\"openai\"`) |\n| `defaultModel` | 끈 | - | 기본 모델 ID |\n| `defaultThinkingLevel` | 끈 | - | `\"off\"`, `\"minimal\"`, `\"low\"`, `\"medium\"`, `\"high\"`, `\"xhigh\"`, `\"max\"` |\n| `hideThinkingBlock` | 부울 | `false` | 출력에서 사고 블록 숨기기 |\n| `showCacheMissNotices` | 부울 | `false` | 중요한 프롬프트 캐시 누락에 대한 기록 알림 표시 |\n| `thinkingBudgets` | 물체 | - | 사고 수준에 따른 맞춤형 토큰 예산 |\n\n#### 생각예산\n\n```json\n{\n  \"thinkingBudgets\": {\n    \"minimal\": 1024,\n    \"low\": 4096,\n    \"medium\": 10240,\n    \"high\": 32768\n  }\n}\n```\n\n### UI 및 디스플레이\n\n| 환경 | 유형 | 기본 | 설명 |\n|---------|------|---------|-------------|\n| `theme` | 끈 | `\"dark\"` | 테마 이름(`\"dark\"`, `\"light\"` 또는 맞춤설정) |\n| `externalEditor` | 끈 | `$VISUAL`, `$EDITOR`, Windows에서는 메모장, 다른 곳에서는 `nano` | Ctrl+G 외부 편집기 명령; 환경 변수보다 우선합니다. |\n| `quietStartup` | 부울 | `false` | 시작 헤더 숨기기 |\n| `defaultProjectTrust` | 끈 | `\"ask\"` | 대체 프로젝트 신뢰 동작: `\"ask\"`, `\"always\"` 또는 `\"never\"`. 전역 설정만 |\n| `collapseChangelog` | 부울 | `false` | 업데이트 후 요약된 변경 로그 표시 |\n| `enableInstallTelemetry` | 부울 | `true` | 첫 번째 설치 또는 변경 로그 감지 업데이트 후 익명의 설치/업데이트 버전 핑을 보냅니다. 업데이트 확인을 제어하지 않습니다. |\n| `enableAnalytics` | 부울 | `false` | 분석 데이터 공유를 선택하세요. 현재는 최초 실험 설정 중에만 요청됩니다(`PI_EXPERIMENTAL=1`). |\n| `trackingId` | 끈 | - | `enableAnalytics`가 켜져 있을 때 생성되는 Analytics 추적 식별자 |\n| `doubleEscapeAction` | 끈 | `\"tree\"` | 이중 이스케이프 작업: `\"tree\"`, `\"fork\"` 또는 `\"none\"` |\n| `treeFilterMode` | 끈 | `\"default\"` | `/tree`에 대한 기본 필터: `\"default\"`, `\"no-tools\"`, `\"user-only\"`, `\"labeled-only\"`, `\"all\"` |\n| `editorPaddingX` | 숫자 | `0` | 입력 편집기의 가로 패딩(0-3) |\n| `outputPad` | 숫자 | `1` | 사용자 메시지, 보조 메시지, 생각에 대한 가로 패딩(0 또는 1) |\n| `autocompleteMaxVisible` | 숫자 | `5` | 자동 완성 드롭다운에 표시되는 최대 항목(3-20) |\n| `showHardwareCursor` | 부울 | `false` | TUI가 IME 지원을 위해 위치를 지정하는 동안 터미널 커서를 표시합니다. |\n| `tuiMode` | 끈 | `\"regular\"` | 대화형 TUI 모드: `\"regular\"` 또는 실험적 `\"fullscreen\"`. `/settings`의 변경사항은 즉시 적용됩니다. `--tui-mode` 시작 시 이 설정을 재정의합니다. |\n| `fullscreenExitOutput` | 끈 | `\"transcript\"` | 전체 화면 종료 출력: `\"transcript\"`는 최종 기록과 이력서 힌트를 인쇄하고, `\"resume-hint\"`는 이전 화면을 복원하고 이력서 힌트만 인쇄합니다. 일반 TUI 모드에서는 효과가 없습니다. |\n| `fullscreenScrollbar` | 끈 | `\"auto\"` | 전체 화면 기록 스크롤 막대: `\"auto\"`는 스크롤하는 동안 일시적으로 표시하고, `\"always\"`는 가장 오른쪽 열을 예약하여 표시되도록 유지하며, `\"hidden\"`는 숨깁니다. 일반 TUI 모드에서는 효과가 없습니다. |\n\nVS Code의 경우 `--wait`를 포함하면 편집기가 종료된 후 pi가 다시 시작됩니다.\n\n```json\n{\n  \"externalEditor\": \"code --wait\"\n}\n```\n\n### 원격 분석 및 업데이트 확인\n\n`enableInstallTelemetry`는 `https://pi.dev/api/report-install`에 대한 익명 설치/업데이트 핑만 제어합니다. 원격 분석을 옵트아웃해도 업데이트 확인이 비활성화되지는 않습니다. Pi는 여전히 `https://pi.dev/api/latest-version`를 가져와서 최신 버전을 찾을 수 있습니다.\n\n`PI_SKIP_VERSION_CHECK=1`를 설정하면 Pi 버전 업데이트 확인이 비활성화됩니다. 업데이트 확인, 패키지 업데이트 확인, 설치/업데이트 원격 측정을 포함하여 여기에 설명된 모든 시작 네트워크 작업을 비활성화하려면 `--offline` 또는 `PI_OFFLINE=1`를 사용하세요.\n\n### 회로망\n\n| 환경 | 유형 | 기본 | 설명 |\n|---------|------|---------|-------------|\n| `httpProxy` | 끈 | - | `HTTP_PROXY` 및 `HTTPS_PROXY`로 적용되는 HTTP 프록시 URL입니다. 전역 설정에만 해당됩니다. |\n\n```json\n{\n  \"httpProxy\": \"http://127.0.0.1:7890\"\n}\n```\n\n### 경고\n\n| 환경 | 유형 | 기본 | 설명 |\n|---------|------|---------|-------------|\n| `warnings.anthropicExtraUsage` | 부울 | `true` | Anthropic 구독 인증이 유료 추가 사용량을 사용할 수 있는 경우 경고 표시 |\n\n```json\n{\n  \"warnings\": {\n    \"anthropicExtraUsage\": false\n  }\n}\n```\n\n### 압축\n\n| 환경 | 유형 | 기본 | 설명 |\n|---------|------|---------|-------------|\n| `compaction.enabled` | 부울 | `true` | 자동 압축 활성화 |\n| `compaction.reserveTokens` | 숫자 | `16384` | LLM 응답용으로 예약된 토큰 |\n| `compaction.keepRecentTokens` | 숫자 | `20000` | 보관할 최근 토큰(요약되지 않음) |\n\n```json\n{\n  \"compaction\": {\n    \"enabled\": true,\n    \"reserveTokens\": 16384,\n    \"keepRecentTokens\": 20000\n  }\n}\n```\n\n### 지점 요약\n\n| 환경 | 유형 | 기본 | 설명 |\n|---------|------|---------|-------------|\n| `branchSummary.reserveTokens` | 숫자 | `16384` | branch summarization용으로 예약된 토큰 |\n| `branchSummary.skipPrompt` | 부울 | `false` | \"분기를 요약하시겠습니까?\"를 건너뛰세요. `/tree` 탐색 시 프롬프트(기본값은 요약 없음) |\n\n### 다시 해 보다\n\n| 환경 | 유형 | 기본 | 설명 |\n|---------|------|---------|-------------|\n| `retry.enabled` | 부울 | `true` | 일시적인 오류에 대해 자동 에이전트 수준 재시도 활성화 |\n| `retry.maxRetries` | 숫자 | `3` | 최대 에이전트 수준 재시도 횟수 |\n| `retry.baseDelayMs` | 숫자 | `2000` | 에이전트 수준 지수 백오프에 대한 기본 지연(2초, 4초, 8초) |\n| `retry.provider.timeoutMs` | 숫자 | SDK 기본값 | Provider/SDK 요청 시간 제한(밀리초) |\n| `retry.provider.maxRetries` | 숫자 | `0` | Provider/SDK 재시도 횟수 |\n| `retry.provider.maxRetryDelayMs` | 숫자 | `60000` | 실패하기 전 최대 서버 요청 지연 시간(60초) |\n\n공급자가 `retry.provider.maxRetryDelayMs`보다 긴 재시도 지연을 요청하면 조용히 기다리는 대신 정보 오류와 함께 요청이 즉시 실패합니다. 제한을 비활성화하려면 `0`로 설정하세요.\n\n공급자 수준 재시도가 명시적으로 필요한 경우가 아니면 `retry.provider.maxRetries`를 `0`로 유지하세요. `0` 이상으로 설정하면 SDK/공급자 재시도가 사용 제한 초과 오류를 Pi에서 확인하기 전에 처리할 수 있으며, 이로 인해 일부 상황에서 공급자 할당량이 재설정될 때까지 에이전트가 차단될 수 있습니다.\n\n```json\n{\n  \"retry\": {\n    \"enabled\": true,\n    \"maxRetries\": 3,\n    \"baseDelayMs\": 2000,\n    \"provider\": {\n      \"timeoutMs\": 3600000,\n      \"maxRetries\": 0,\n      \"maxRetryDelayMs\": 60000\n    }\n  }\n}\n```\n\n### 메시지 전달\n\n| 환경 | 유형 | 기본 | 설명 |\n|---------|------|---------|-------------|\n| `steeringMode` | 끈 | `\"one-at-a-time\"` | 조정 메시지 전송 방법: `\"all\"` 또는 `\"one-at-a-time\"` |\n| `followUpMode` | 끈 | `\"one-at-a-time\"` | 후속 메시지 전송 방법: `\"all\"` 또는 `\"one-at-a-time\"` |\n| `transport` | 끈 | `\"auto\"` | 여러 전송을 지원하는 제공업체가 선호하는 전송: `\"sse\"`, `\"websocket\"`, `\"websocket-cached\"` 또는 `\"auto\"` |\n| `httpIdleTimeoutMs` | 숫자 | `300000` | HTTP 헤더/본문 유휴 시간 제한(밀리초). 명시적인 스트림 유휴 시간 제한이 있는 공급자에서도 사용됩니다. 비활성화하려면 `0`로 설정하세요. |\n| `websocketConnectTimeoutMs` | 숫자 | `15000` | WebSocket 전송을 지원하는 공급자에 대한 WebSocket 연결/열기 핸드셰이크 시간 제한(밀리초)입니다. 비활성화하려면 `0`로 설정하세요. |\n\n### 터미널 및 이미지\n\n| 환경 | 유형 | 기본 | 설명 |\n|---------|------|---------|-------------|\n| `terminal.showImages` | 부울 | `true` | 터미널에 이미지 표시(지원되는 경우) |\n| `terminal.imageWidthCells` | 숫자 | `60` | 터미널 셀의 기본 인라인 이미지 너비 |\n| `terminal.clearOnShrink` | 부울 | `false` | 콘텐츠가 축소되면 빈 행 지우기(깜박임이 발생할 수 있음) |\n| `images.autoResize` | 부울 | `true` | 이미지 크기를 최대 2000x2000으로 조정합니다. `@file` 첨부파일, `read` 및 도구에서 반환한 이미지에 적용됩니다. |\n| `images.blockImages` | 부울 | `false` | 모든 이미지가 LLM으로 전송되지 않도록 차단 |\n\n### 껍데기\n\n| 환경 | 유형 | 기본 | 설명 |\n|---------|------|---------|-------------|\n| `shellPath` | 끈 | - | 사용자 정의 셸 경로(예: Windows의 Cygwin용) 홈 디렉토리에 대해 선행 `~` 지원 |\n| `shellCommandPrefix` | 끈 | - | 모든 bash 명령의 접두사(예: `\"shopt -s expand_aliases\"`) |\n| `npmCommand` | 끈[] | - | npm 패키지 조회/설치 작업에 사용되는 명령 argv(예: `[\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"]`) |\n\n```json\n{\n  \"npmCommand\": [\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"]\n}\n```\n\n`npmCommand`는 git 패키지 내부의 설치, 제거 및 종속성 설치를 포함한 모든 npm 패키지 관리자 작업에 사용됩니다. 사용자 범위 npm 패키지는 `~/.pi/agent/npm/` 아래에 설치됩니다. 프로젝트 범위 npm 패키지는 `.pi/npm/` 아래에 설치됩니다. 프로세스가 시작되어야 하는 것과 똑같이 argv 스타일 항목을 사용하십시오. `npmCommand`가 구성되면 git 패키지 종속성 설치는 일반 `install`를 사용하여 래퍼 또는 대체 패키지 관리자의 npm 관련 플래그를 방지합니다.\n\n### 세션\n\n| 환경 | 유형 | 기본 | 설명 |\n|---------|------|---------|-------------|\n| `sessionDir` | 끈 | - | 세션 파일이 저장되는 디렉터리입니다. 절대 또는 상대 경로와 `~`를 허용합니다. |\n\n```json\n{ \"sessionDir\": \".pi/sessions\" }\n```\n\n여러 소스가 세션 디렉터리를 지정하는 경우 settings.json에서 우선 순위는 `--session-dir`, `PI_CODING_AGENT_SESSION_DIR`, `sessionDir`입니다.\n\n### 모델 사이클링\n\n| 환경 | 유형 | 기본 | 설명 |\n|---------|------|---------|-------------|\n| `enabledModels` | 끈[] | - | Ctrl+P 순환을 위한 모델 패턴(`--models` CLI 플래그와 동일한 형식) |\n\n```json\n{\n  \"enabledModels\": [\"claude-*\", \"gpt-4o\", \"gemini-2*\"]\n}\n```\n\n### Markdown\n\n| 환경 | 유형 | 기본 | 설명 |\n|---------|------|---------|-------------|\n| `markdown.codeBlockIndent` | 끈 | `\"  \"` | 코드 블록 들여쓰기 |\n| `markdown.mermaid` | 끈 | `\"streaming\"` | 인어 렌더링 모드: `\"off\"`, `\"final\"` 또는 `\"streaming\"` |\n\n### 자원\n\n이러한 설정은 확장 프로그램, 스킬, 프롬프트 및 테마를 로드할 위치를 정의합니다.\n\n`~/.pi/agent/settings.json`의 경로는 `~/.pi/agent`를 기준으로 결정됩니다. `.pi/settings.json`의 경로는 `.pi`를 기준으로 결정됩니다. 절대 경로와 `~`가 지원됩니다.\n\n| 환경 | 유형 | 기본 | 설명 |\n|---------|------|---------|-------------|\n| `packages` | 정렬 | `[]` | npm/git 리소스를 로드할 패키지 |\n| `extensions` | 끈[] | `[]` | 로컬 확장 파일 경로 또는 디렉터리 |\n| `skills` | 끈[] | `[]` | 로컬 기술 파일 경로 또는 디렉터리 |\n| `prompts` | 끈[] | `[]` | 로컬 프롬프트 템플릿 경로 또는 디렉터리 |\n| `themes` | 끈[] | `[]` | 로컬 테마 파일 경로 또는 디렉터리 |\n| `enableSkillCommands` | 부울 | `true` | `/skill:name` 명령어로 스킬 등록 |\n\n배열은 glob 패턴과 제외를 지원합니다. 제외하려면 `!pattern`를 사용하세요. 정확한 경로를 강제로 포함하려면 `+path`를 사용하고, 정확한 경로를 강제로 제외하려면 `-path`를 사용하세요.\n\n#### 패키지\n\n문자열 형식은 패키지에서 모든 리소스를 로드합니다.\n\n```json\n{\n  \"packages\": [\"pi-skills\", \"@org/my-extension\"]\n}\n```\n\n개체 양식은 로드할 리소스를 필터링합니다.\n\n```json\n{\n  \"packages\": [\n    {\n      \"source\": \"pi-skills\",\n      \"skills\": [\"brave-search\", \"transcribe\"],\n      \"extensions\": []\n    }\n  ]\n}\n```\n\n패키지 관리에 대한 자세한 내용은 [packages.md](packages.md)를 참조하세요.\n\n## 예\n\n```json\n{\n  \"defaultProvider\": \"anthropic\",\n  \"defaultModel\": \"claude-sonnet-4-20250514\",\n  \"defaultThinkingLevel\": \"medium\",\n  \"theme\": \"dark\",\n  \"compaction\": {\n    \"enabled\": true,\n    \"reserveTokens\": 16384,\n    \"keepRecentTokens\": 20000\n  },\n  \"retry\": {\n    \"enabled\": true,\n    \"maxRetries\": 3\n  },\n  \"enabledModels\": [\"claude-*\", \"gpt-4o\"],\n  \"warnings\": {\n    \"anthropicExtraUsage\": true\n  },\n  \"packages\": [\"pi-skills\"]\n}\n```\n\n## 프로젝트 재정의\n\n프로젝트 설정(`.pi/settings.json`)은 전역 설정보다 우선 적용됩니다. 중첩된 개체가 병합됩니다.\n\n```json\n// ~/.pi/agent/settings.json (global)\n{\n  \"theme\": \"dark\",\n  \"compaction\": { \"enabled\": true, \"reserveTokens\": 16384 }\n}\n\n// .pi/settings.json (project)\n{\n  \"compaction\": { \"reserveTokens\": 8192 }\n}\n\n// Result\n{\n  \"theme\": \"dark\",\n  \"compaction\": { \"enabled\": true, \"reserveTokens\": 8192 }\n}\n```","sourceFile":"settings.md"},"shell-aliases":{"title":"쉘 별칭","markdown":"Pi는 기본적으로 별칭을 확장하지 않는 비대화형 모드(`bash -c`)에서 bash를 실행합니다.\n\n셸 별칭을 활성화하려면 `~/.pi/agent/settings.json`에 다음을 추가하세요.\n\n```json\n{\n  \"shellCommandPrefix\": \"shopt -s expand_aliases\\neval \\\"$(grep '^alias ' ~/.zshrc)\\\"\"\n}\n```\n\n쉘 구성에 맞게 경로(`~/.zshrc`, `~/.bashrc` 등)를 조정하세요.","sourceFile":"shell-aliases.md"},"skills":{"title":"Skills","markdown":"> 파이는 기술을 만들 수 있습니다. 귀하의 사용 사례에 맞게 구축하도록 요청하세요.\n\n\nSkills는 에이전트가 요청 시 로드하는 자체 포함 기능 패키지입니다. 기술은 특정 작업에 대한 특수 워크플로, 설정 지침, 도우미 스크립트 및 참조 문서를 제공합니다.\n\nPi는 [Agent Skills standard](https://agentskills.io/specification)를 구현하여 대부분의 위반 사항에 대해 경고하지만 관대함을 유지합니다. Pi 표준에서는 이를 허용하지 않더라도 기술 이름이 상위 디렉터리와 다를 수 있도록 허용합니다. 이 규칙은 여러 에이전트 하네스에서 사용되는 공유 기술 디렉터리에 적합하지 않습니다.\n\n## 목차\n\n- [Locations](#locations)\n- [How Skills Work](#how-skills-work)\n- [Skill Commands](#skill-commands)\n- [Skill Structure](#skill-structure)\n- [Frontmatter](#frontmatter)\n- [Validation](#validation)\n- [Example](#example)\n- [Skill Repositories](#skill-repositories)\n\n## 위치\n\n> **보안:** Skills 모델에 모든 작업을 수행하도록 지시할 수 있으며 모델이 호출하는 실행 가능한 코드가 포함될 수 있습니다. 사용 전 스킬 내용을 검토하세요.\n\nPi 다음에서 스킬을 로드합니다.\n\n- 글로벌:\n  - `~/.pi/agent/skills/`\n  - `~/.agents/skills/`\n- 프로젝트(프로젝트를 신뢰한 후에만):\n  - `.pi/skills/`\n  - `.agents/skills/` `cwd` 및 상위 디렉터리(최대 git repo 루트 또는 repo에 없는 경우 파일 시스템 루트)\n- 패키지: `skills/` 디렉토리 또는 `package.json`의 `pi.skills` 항목\n- 설정: `skills` 파일 또는 디렉터리가 포함된 배열\n- CLI: `--skill <path>` (반복 가능, `--no-skills`을 사용해도 추가 가능)\n\n검색 규칙:\n- `~/.pi/agent/skills/` 및 `.pi/skills/`에서는 직접 루트 `.md` 파일이 개별 스킬로 검색됩니다.\n- 모든 스킬 위치에서 `SKILL.md`가 포함된 디렉터리가 반복적으로 검색됩니다.\n- `~/.agents/skills/` 및 프로젝트 `.agents/skills/`에서는 루트 `.md` 파일이 무시됩니다.\n\n`--no-skills`로 검색을 비활성화합니다(명시적 `--skill` 경로는 계속 로드됨).\n\n### 다른 하네스의 Skills 사용\n\nClaude Code 또는 OpenAI Codex의 기술을 사용하려면 해당 디렉터리를 설정에 추가하세요.\n\n```json\n{\n  \"skills\": [\n    \"~/.claude/skills\",\n    \"~/.codex/skills\"\n  ]\n}\n```\n\n프로젝트 수준 Claude Code 기술의 경우 `.pi/settings.json`에 추가하세요.\n\n```json\n{\n  \"skills\": [\"../.claude/skills\"]\n}\n```\n\n## Skills 작동 방식\n\n1. 시작 시 pi는 기술 위치를 스캔하고 이름과 설명을 추출합니다.\n2. 시스템 프롬프트에는 [specification](https://agentskills.io/integrate-skills)에 따라 XML 형식으로 사용 가능한 기술이 포함되어 있습니다.\n3. 작업이 일치하면 에이전트는 `read`를 사용하여 전체 SKILL.md를 로드합니다(모델이 항상 이 작업을 수행하는 것은 아닙니다. 프롬프트를 사용하거나 `/skill:name`를 사용하여 강제 실행).\n4. 에이전트는 스크립트 및 자산을 참조하기 위한 상대 경로를 사용하여 지침을 따릅니다.\n\n이는 점진적인 공개입니다. 설명만 항상 상황에 맞게 제공되며 전체 지침은 요청 시 로드됩니다.\n\n## 스킬 명령\n\nSkills `/skill:name` 명령으로 등록:\n\n```bash\n/skill:brave-search           # Load and execute the skill\n/skill:pdf-tools extract      # Load skill with arguments\n```\n\n명령 뒤의 인수는 스킬 내용에 `User: <args>`로 추가됩니다.\n\n대화형 모드에서 `/settings` 또는 `settings.json`를 통해 스킬 명령을 전환하세요.\n\n```json\n{\n  \"enableSkillCommands\": true\n}\n```\n\n## 스킬 구조\n\n스킬은 `SKILL.md` 파일이 포함된 디렉터리입니다. 다른 모든 것은 자유 형식입니다.\n\n```\nmy-skill/\n├── SKILL.md              # Required: frontmatter + instructions\n├── scripts/              # Helper scripts\n│   └── process.sh\n├── references/           # Detailed docs loaded on-demand\n│   └── api-reference.md\n└── assets/\n    └── template.json\n```\n\n### SKILL.md 형식\n\n````markdown\n---\nname: my-skill\ndescription: What this skill does and when to use it. Be specific.\n---\n\n# My Skill\n\n## Setup\n\nRun once before first use:\n```bash\ncd /path/to/skill && npm 설치\n```\n\n## Usage\n\n```bash\n./scripts/process.sh <input>\n```\n````\n\n기술 디렉터리의 상대 경로를 사용하십시오.\n\n```markdown\nSee [the reference guide](references/REFERENCE.md) for details.\n```\n\n## 머리말\n\n[Agent Skills specification](https://agentskills.io/specification#frontmatter-required)에 따라:\n\n| 필드 | 필수의 | 설명 |\n|-------|----------|-------------|\n| `name` | 예 | 최대 64자. 소문자 a~z, 0~9, 하이픈. 표준과 달리 Pi는 표준 요구 사항이 공유 스킬 디렉터리에 적합하지 않기 때문에 상위 디렉터리와 일치할 것을 요구하지 않습니다. |\n| `description` | 예 | 최대 1024자. 스킬의 기능과 사용 시기. |\n| `license` | 아니요 | 라이센스 이름 또는 번들 파일에 대한 참조입니다. |\n| `compatibility` | 아니요 | 최대 500자. 환경 요구 사항. |\n| `metadata` | 아니요 | 임의의 키-값 매핑. |\n| `allowed-tools` | 아니요 | 사전 승인된 도구의 공백으로 구분된 목록(실험적)입니다. |\n| `disable-model-invocation` | 아니요 | `true`일 경우 시스템 프롬프트에서 스킬이 숨겨집니다. 사용자는 `/skill:name`를 사용해야 합니다. |\n\n### 이름 규칙\n\n- 1~64자\n- 소문자, 숫자, 하이픈만 가능\n- 선행/후행 하이픈 없음\n- 연속된 하이픈이 없습니다.\nPi에는 상위 디렉토리와 일치하는 이름이 필요하지 않습니다. Agent Skills 표준은 그렇습니다. 그러나 해당 요구 사항은 여러 도구에서 사용되는 공유 스킬 디렉터리에 대해서는 차선책입니다.\n\n유효: `pdf-processing`, `data-analysis`, `code-review`\n유효하지 않음: `PDF-Processing`, `-pdf`, `pdf--processing`\n\n### 설명 모범 사례\n\n설명은 상담원이 스킬을 로드하는 시기를 결정합니다. 구체적으로 말하세요.\n\n좋은:\n```yaml\ndescription: Extracts text and tables from PDF files, fills PDF forms, and merges multiple PDFs. Use when working with PDF documents.\n```\n\n가난한:\n```yaml\ndescription: Helps with PDFs.\n```\n\n## 확인\n\nPi는 Agent Skills 표준에 따라 기술을 검증합니다. 대부분의 문제는 경고를 생성하지만 여전히 스킬을 로드합니다.\n\n- 이름이 64자를 초과하거나 잘못된 문자가 포함되어 있습니다.\n- 이름이 하이픈으로 시작/끝나거나 하이픈이 연속되어 있습니다.\n- 설명이 1,024자를 초과합니다.\n\n알 수 없는 머리말 필드는 무시됩니다.\n\n**예외:** 설명이 누락된 Skills는 로드되지 않습니다.\n\n이름 충돌(다른 위치의 동일한 이름)은 경고하고 첫 번째 발견된 스킬을 유지합니다.\n\n## 예\n\n```\nbrave-search/\n├── SKILL.md\n├── search.js\n└── content.js\n```\n\n**SKILL.md:**\n````markdown\n---\nname: brave-search\ndescription: Web search and content extraction via Brave Search API. Use for searching documentation, facts, or any web content.\n---\n\n# Brave Search\n\n## Setup\n\n```bash\ncd /path/to/brave-search && npm 설치\n```\n\n## Search\n\n```bash\n./search.js \"query\" # 기본 검색\n./search.js \"query\" --content # 페이지 콘텐츠 포함\n```\n\n## Extract Page Content\n\n```bash\n./content.js https://example.com\n```\n````\n\n## 기술 저장소\n\n- [Anthropic Skills](https://github.com/anthropics/skills) - 문서 처리(docx, pdf, pptx, xlsx), 웹 개발\n- [Pi Skills](https://github.com/badlogic/pi-skills) - 웹 검색, 브라우저 자동화, Google API, 텍스트 변환","sourceFile":"skills.md"},"terminal-setup":{"title":"터미널 설정","markdown":"Pi는 안정적인 수정자 키 감지를 위해 [Kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/)를 사용합니다. 대부분의 최신 터미널은 이 프로토콜을 지원하지만 일부는 구성이 필요합니다.\n\n## 키티, iTerm2\n\n즉시 사용 가능\n\n## 애플 터미널\n\nPi 가능한 경우 향상된 키 보고를 활성화합니다. Terminal.app이 여전히 `Shift+Enter`에 대한 일반 Return을 보내는 경우 pi는 로컬 macOS 수정자 대체를 사용하여 해당 Return을 `Shift+Enter`로 처리합니다.\n\n이 대체 방법은 pi가 Terminal.app과 동일한 Mac에서 실행되는 경우에만 작동합니다. 원격 SSH을 통해 로컬 키보드를 감지할 수 없습니다.\n\n## 유령\n\nGhostty 구성에 추가합니다(macOS에서는 `~/Library/Application Support/com.mitchellh.ghostty/config`, Linux에서는 `~/.config/ghostty/config`):\n\n```\nkeybind = alt+backspace=text:\\x1b\\x7f\n```\n\n이전 Claude Code 버전에는 다음 Ghostty 매핑이 추가되었을 수 있습니다.\n\n```\nkeybind = shift+enter=text:\\n\n```\n\n해당 매핑은 원시 줄 바꿈 바이트를 보냅니다. pi 내부에서는 `Ctrl+J`와 구별할 수 없으므로 tmux 및 pi는 더 이상 실제 `shift+enter` 키 이벤트를 볼 수 없습니다.\n\n해당 매핑을 추가한 유일한 이유가 Claude Code 2.x 이상이면 이를 제거할 수 있습니다. 단, 여전히 Ghostty 매핑이 필요한 tmux에서 Claude Code를 사용하려는 경우는 예외입니다.\n\nPi는 `Ctrl+J`를 기본 개행 별칭으로 바인딩하므로 `Shift+Enter`는 추가 파이 구성 없이 다시 매핑을 통해 tmux에서 계속 작동합니다.\n\n## WezTerm\n\nWezTerm은 일반적으로 xterm 수정OtherKeys를 통해 `Shift+Enter`에 대해 즉시 작동합니다. Kitty 키보드 프로토콜을 명시적으로 사용하려면 `~/.wezterm.lua`을 생성하세요.\n\n```lua\nlocal wezterm = require 'wezterm'\nlocal config = wezterm.config_builder()\nconfig.enable_kitty_keyboard = true\nreturn config\n```\n\nmacOS에서 WezTerm은 기본적으로 `Option+Enter`를 전체 화면으로 바인딩합니다. pi 후속 조치 대기열에 `Option+Enter`를 사용하려면 다음 키 재정의를 추가하세요.\n\n```lua\nlocal wezterm = require 'wezterm'\nlocal config = wezterm.config_builder()\nconfig.keys = {\n  {\n    key = 'Enter',\n    mods = 'ALT',\n    action = wezterm.action.SendString('\\x1b[13;3u'),\n  },\n}\nreturn config\n```\n\n`config.keys` 테이블이 이미 있으면 해당 테이블에 항목을 추가하세요.\n\nWSL에서 WezTerm은 IME 후보 창 위치 지정을 위해 눈에 보이는 하드웨어 커서가 필요할 수 있습니다. CJK IME 후보가 텍스트 커서를 따르지 않는 경우 pi를 실행하기 전에 `PI_HARDWARE_CURSOR=1`를 설정하거나 설정에서 `showHardwareCursor`에서 `true`로 설정하세요.\n\n## 기민함\n\nAlacritty는 일반적으로 `Shift+Enter`에서 기본적으로 작동합니다. macOS에서는 `Option+Enter`가 일반 `Enter`로 도착할 수 있습니다. pi 후속 대기열에 `Option+Enter`를 사용하려면 `~/.config/alacritty/alacritty.toml`에 추가하세요.\n\n```toml\n[[keyboard.bindings]]\nkey = \"Enter\"\nmods = \"Alt\"\nchars = \"\\u001b[13;3u\"\n```\n\n구성을 변경한 후 Alacritty를 다시 시작하십시오.\n\n## VS Code(통합 터미널)\n\nVS Code 1.109.5 이상에서는 기본적으로 통합 터미널에서 Kitty 키보드 프로토콜을 활성화하므로 `Shift+Enter`가 즉시 작동합니다.\n\n1.109.5 이전 VS Code 버전에는 `Shift+Enter`에 대한 명시적인 터미널 키 바인딩이 필요합니다.\n\n`keybindings.json` 위치:\n- 맥OS: `~/Library/Application Support/Code/User/keybindings.json`\n- 리눅스: `~/.config/Code/User/keybindings.json`\n- 윈도우: `%APPDATA%\\\\Code\\\\User\\\\keybindings.json`\n\n`keybindings.json`에 추가:\n\n```json\n{\n  \"key\": \"shift+enter\",\n  \"command\": \"workbench.action.terminal.sendSequence\",\n  \"args\": { \"text\": \"\\u001b[13;2u\" },\n  \"when\": \"terminalFocus\"\n}\n```\n\n## 윈도우 터미널\n\npi가 사용하는 수정된 Enter 키를 전달하려면 `settings.json`(Ctrl+Shift+ 또는 설정 → JSON 파일 열기)에 추가하세요.\n\n```json\n{\n  \"actions\": [\n    {\n      \"command\": { \"action\": \"sendInput\", \"input\": \"\\u001b[13;2u\" },\n      \"keys\": \"shift+enter\"\n    },\n    {\n      \"command\": { \"action\": \"sendInput\", \"input\": \"\\u001b[13;3u\" },\n      \"keys\": \"alt+enter\"\n    }\n  ]\n}\n```\n\n- `Shift+Enter` 새 줄을 삽입합니다.\n- Windows 터미널은 기본적으로 `Alt+Enter`를 전체 화면으로 바인딩합니다. 이는 pi가 후속 대기열에 대해 `Alt+Enter`을 수신하는 것을 방지합니다.\n- `Alt+Enter`를 `sendInput`로 다시 매핑하면 실제 키 코드가 대신 pi로 전달됩니다.\n\n`actions` 배열이 이미 있는 경우 해당 배열에 개체를 추가하세요. 이전 전체 화면 동작이 지속되면 Windows 터미널을 완전히 닫았다가 다시 엽니다.\n\n## xfce4 터미널, 터미네이터\n\n이러한 터미널은 이스케이프 시퀀스 지원이 제한되어 있습니다. `Ctrl+Enter` 및 `Shift+Enter`와 같은 수정된 Enter 키는 일반 `Enter`와 구별할 수 없으므로 `submit: [\"ctrl+enter\"]`와 같은 사용자 정의 키 바인딩이 작동하지 않습니다.\n\n최상의 경험을 위해서는 Kitty 키보드 프로토콜을 지원하는 터미널을 사용하십시오.\n- [Kitty](https://sw.kovidgoyal.net/kitty/)\n- [Ghostty](https://ghostty.org/)\n- [WezTerm](https://wezfurlong.org/wezterm/)\n- [iTerm2](https://iterm2.com/)\n- [Alacritty](https://github.com/alacritty/alacritty) (Kitty 프로토콜 지원을 통한 컴파일 필요)\n\n## IntelliJ IDEA(통합 터미널)\n\n내장 터미널은 이스케이프 시퀀스 지원이 제한되어 있습니다. IntelliJ 터미널에서는 Shift+Enter를 Enter와 구별할 수 없습니다.\n\n하드웨어 커서를 표시하려면 pi를 실행하기 전에 `PI_HARDWARE_CURSOR=1`를 설정하세요(호환성을 위해 기본적으로 비활성화되어 있음).\n\n최상의 경험을 위해서는 전용 터미널 에뮬레이터를 사용하는 것이 좋습니다.","sourceFile":"terminal-setup.md"},"termux":{"title":"Termux (안드로이드) 설정","markdown":"Pi는 Android용 터미널 에뮬레이터이자 Linux 환경인 [Termux](https://termux.dev/)를 통해 Android에서 실행됩니다.\n\n## 전제 조건\n\n1. GitHub 또는 F-Droid에서 [Termux](https://github.com/termux/termux-app#installation) 설치(Google Play 아님, 해당 버전은 더 이상 사용되지 않음)\n2. 클립보드 및 기타 장치 통합을 위해 GitHub 또는 F-Droid에서 [Termux:API](https://github.com/termux/termux-api#installation)를 설치하세요.\n\n## 설치\n\n```bash\n# Update packages\npkg update && pkg upgrade\n\n# Install dependencies\npkg install nodejs termux-api git\n\n# Install pi\nnpm install -g --ignore-scripts @earendil-works/pi-coding-agent\n\n# Create config directory\nmkdir -p ~/.pi/agent\n\n# Run pi\npi\n```\n\n## 클립보드 지원\n\n클립보드 작업은 Termux에서 실행될 때 `termux-clipboard-set` 및 `termux-clipboard-get`를 사용합니다. 이 기능이 작동하려면 Termux:API 앱을 설치해야 합니다.\n\nTermux에서는 이미지 클립보드가 지원되지 않습니다(`ctrl+v` 이미지 붙여넣기 기능은 작동하지 않습니다).\n\n## Termux에 대한 AGENTS.md 예\n\n에이전트가 Termux 환경을 이해하는 데 도움이 되도록 `~/.pi/agent/AGENTS.md`를 만듭니다.\n\n````markdown\n# Agent Environment: Termux on Android\n\n## Location\n- **OS**: Android (Termux terminal emulator)\n- **Home**: `/data/data/com.termux/files/home`\n- **Prefix**: `/data/data/com.termux/files/usr`\n- **Shared storage**: `/storage/emulated/0` (Downloads, Documents, etc.)\n\n## Opening URLs\n```bash\ntermux-open-url \"https://example.com\"\n```\n\n## Opening Files\n```bash\ntermux-open file.pdf # 기본 앱으로 열립니다.\ntermux-open --chooser image.jpg # 앱 선택\n```\n\n## Clipboard\n```bash\ntermux-clipboard-set \"text\" # 복사\ntermux-clipboard-get # 붙여넣기\n```\n\n## Notifications\n```bash\ntermux-notification -t \"제목\" -c \"콘텐츠\"\n```\n\n## Device Info\n```bash\ntermux-battery-status # 배터리 정보\ntermux-wifi-connectioninfo # WiFi 정보\ntermux-telephony-deviceinfo # 장치 정보\n```\n\n## Sharing\n```bash\ntermux-share -a file.txt 보내기 # 파일 공유\n```\n\n## Other Useful Commands\n```bash\ntermux-toast \"message\" # 빠른 토스트 팝업\ntermux-vibrate # 진동 장치\ntermux-tts-speak \"hello\" # 텍스트 음성 변환\ntermux-camera-photo out.jpg # 사진 찍기\n```\n\n## Notes\n- Termux:API app must be installed for `termux-*` commands\n- Use `pkg install termux-api` for the command-line tools\n- Storage permission needed for `/storage/emulated/0` access\n````\n\n## 제한사항\n\n- **이미지 클립보드 없음**: Termux 클립보드 API 텍스트만 지원\n- **네이티브 바이너리 없음**: 일부 선택적 네이티브 종속성(예: 클립보드 모듈)은 Android ARM64에서 사용할 수 없으며 설치 중에 건너뜁니다.\n- **저장소 액세스**: `/storage/emulated/0`의 파일(다운로드 등)에 액세스하려면 `termux-setup-storage`을 한 번 실행하여 권한을 부여하세요.\n\n## 문제 해결\n\n### 클립보드가 작동하지 않음\n\n두 앱이 모두 설치되어 있는지 확인하세요.\n1. Termux (GitHub 또는 F-Droid에서)\n2. Termux:API (GitHub 또는 F-Droid에서)\n\n그런 다음 CLI 도구를 설치합니다.\n```bash\npkg install termux-api\n```\n\n### 공유 저장장치에 대한 권한이 거부되었습니다.\n\n한 번 실행하여 스토리지 권한을 부여합니다.\n```bash\ntermux-setup-storage\n```\n\n### Node.js 설치 문제\n\nnpm가 실패하면 캐시를 지워보세요.\n```bash\nnpm cache clean --force\n```","sourceFile":"termux.md"},"themes":{"title":"테마","markdown":"> pi는 테마를 만들 수 있습니다. 귀하의 설정을 위해 하나를 구축하도록 요청하십시오.\n\n\n테마는 TUI의 색상을 정의하는 JSON 파일입니다.\n\n## 목차\n\n- [Locations](#locations)\n- [Selecting a Theme](#selecting-a-theme)\n- [Creating a Custom Theme](#creating-a-custom-theme)\n- [Theme Format](#theme-format)\n- [Color Tokens](#color-tokens)\n- [Color Values](#color-values)\n- [Tips](#tips)\n\n## 위치\n\nPi 다음에서 테마를 로드합니다.\n\n- 내장: `dark`, `light`\n- 글로벌: `~/.pi/agent/themes/*.json`\n- 프로젝트: `.pi/themes/*.json` (프로젝트를 신뢰한 후에만)\n- 패키지: `themes/` 디렉토리 또는 `package.json`의 `pi.themes` 항목\n- 설정: `themes` 파일 또는 디렉터리가 포함된 배열\n- CLI: `--theme <path>` (반복 가능)\n\n`--no-themes`로 검색을 비활성화하세요.\n\n## 테마 선택\n\n`/settings` 또는 `settings.json`를 통해 테마를 선택하세요.\n\n```json\n{\n  \"theme\": \"my-theme\"\n}\n```\n\n처음 실행 시 pi는 터미널 배경을 감지하고 기본값은 `dark` 또는 `light`입니다.\n\n## 사용자 정의 테마 만들기\n\n1. 테마 파일을 만듭니다.\n\n```bash\nmkdir -p ~/.pi/agent/themes\nvim ~/.pi/agent/themes/my-theme.json\n```\n\n2. 필요한 모든 색상으로 테마를 정의합니다([Color Tokens](#color-tokens) 참조).\n\n```json\n{\n  \"$schema\": \"https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json\",\n  \"name\": \"my-theme\",\n  \"vars\": {\n    \"primary\": \"#00aaff\",\n    \"secondary\": 242\n  },\n  \"colors\": {\n    \"accent\": \"primary\",\n    \"border\": \"primary\",\n    \"borderAccent\": \"#00ffff\",\n    \"borderMuted\": \"secondary\",\n    \"success\": \"#00ff00\",\n    \"error\": \"#ff0000\",\n    \"warning\": \"#ffff00\",\n    \"muted\": \"secondary\",\n    \"dim\": 240,\n    \"text\": \"\",\n    \"thinkingText\": \"secondary\",\n    \"selectedBg\": \"#2d2d30\",\n    \"scrollbarThumb\": \"#555566\",\n    \"userMessageBg\": \"#2d2d30\",\n    \"userMessageText\": \"\",\n    \"customMessageBg\": \"#2d2d30\",\n    \"customMessageText\": \"\",\n    \"customMessageLabel\": \"primary\",\n    \"toolPendingBg\": \"#1e1e2e\",\n    \"toolSuccessBg\": \"#1e2e1e\",\n    \"toolErrorBg\": \"#2e1e1e\",\n    \"toolTitle\": \"primary\",\n    \"toolOutput\": \"\",\n    \"mdHeading\": \"#ffaa00\",\n    \"mdLink\": \"primary\",\n    \"mdLinkUrl\": \"secondary\",\n    \"mdCode\": \"#00ffff\",\n    \"mdCodeBlock\": \"\",\n    \"mdCodeBlockBorder\": \"secondary\",\n    \"mdQuote\": \"secondary\",\n    \"mdQuoteBorder\": \"secondary\",\n    \"mdHr\": \"secondary\",\n    \"mdListBullet\": \"#00ffff\",\n    \"toolDiffAdded\": \"#00ff00\",\n    \"toolDiffRemoved\": \"#ff0000\",\n    \"toolDiffContext\": \"secondary\",\n    \"syntaxComment\": \"secondary\",\n    \"syntaxKeyword\": \"primary\",\n    \"syntaxFunction\": \"#00aaff\",\n    \"syntaxVariable\": \"#ffaa00\",\n    \"syntaxString\": \"#00ff00\",\n    \"syntaxNumber\": \"#ff00ff\",\n    \"syntaxType\": \"#00aaff\",\n    \"syntaxOperator\": \"primary\",\n    \"syntaxPunctuation\": \"secondary\",\n    \"thinkingOff\": \"secondary\",\n    \"thinkingMinimal\": \"primary\",\n    \"thinkingLow\": \"#00aaff\",\n    \"thinkingMedium\": \"#00ffff\",\n    \"thinkingHigh\": \"#ff00ff\",\n    \"thinkingXhigh\": \"#ff0000\",\n    \"thinkingMax\": \"#ff0088\",\n    \"bashMode\": \"#ffaa00\"\n  }\n}\n```\n\n3. `/settings`를 통해 테마를 선택하세요.\n\n**핫 리로드:** 현재 활성 사용자 정의 테마 파일을 편집할 때 pi는 즉각적인 시각적 피드백을 위해 자동으로 이를 다시 로드합니다.\n\n## 테마 형식\n\n```json\n{\n  \"$schema\": \"https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json\",\n  \"name\": \"my-theme\",\n  \"vars\": {\n    \"blue\": \"#0066cc\",\n    \"gray\": 242\n  },\n  \"colors\": {\n    \"accent\": \"blue\",\n    \"muted\": \"gray\",\n    \"text\": \"\",\n    ...\n  }\n}\n```\n\n- `name`는 필수이고 고유해야 하며 `/`를 포함할 수 없습니다.\n- `vars`는 선택사항입니다. 여기에서 재사용 가능한 색상을 정의한 다음 `colors`에서 참조하세요.\n- `colors` 51개의 필수 토큰을 모두 정의해야 합니다. `thinkingMax`는 선택사항이며 `thinkingXhigh`로 대체됩니다. `scrollbarThumb`는 선택사항이며 `selectedBg`로 대체됩니다.\n\n`$schema` 필드는 편집기 자동 완성 및 유효성 검사를 활성화합니다.\n\n## 컬러 토큰\n\n모든 테마는 51개의 필수 색상 토큰을 모두 정의해야 합니다. `thinkingMax` 및 `scrollbarThumb`는 기존 테마와의 호환성을 위한 선택 사항입니다. 생략하면 각각 `thinkingXhigh` 및 `selectedBg`를 사용합니다.\n\n### 핵심 UI(11가지 색상)\n\n| 토큰 | 목적 |\n|-------|---------|\n| `accent` | 기본 악센트(로고, 선택한 항목, 커서) |\n| `border` | 일반 테두리 |\n| `borderAccent` | 강조된 테두리 |\n| `borderMuted` | 미묘한 테두리(편집기) |\n| `success` | 성공 상태 |\n| `error` | 오류 상태 |\n| `warning` | 경고 상태 |\n| `muted` | 보조 텍스트 |\n| `dim` | 3차 텍스트 |\n| `text` | 기본 텍스트(보통 `\"\"`) |\n| `thinkingText` | 생각 블록 텍스트 |\n\n### 배경 및 콘텐츠(필수 11개, 선택 1개)\n\n| 토큰 | 목적 |\n|-------|---------|\n| `selectedBg` | 선택된 라인 배경 |\n| `scrollbarThumb` | 전체 화면 스크롤 막대 썸 배경; 선택사항, `selectedBg`로 대체 |\n| `userMessageBg` | 사용자 메시지 배경 |\n| `userMessageText` | 사용자 메시지 텍스트 |\n| `customMessageBg` | 확장 메시지 배경 |\n| `customMessageText` | 확장 메시지 텍스트 |\n| `customMessageLabel` | 확장 메시지 라벨 |\n| `toolPendingBg` | 도구 상자(보류 중) |\n| `toolSuccessBg` | 도구 상자(성공) |\n| `toolErrorBg` | 도구 상자(오류) |\n| `toolTitle` | 도구 제목 |\n| `toolOutput` | 도구 출력 텍스트 |\n\n### Markdown (10가지 색상)\n\n| 토큰 | 목적 |\n|-------|---------|\n| `mdHeading` | 제목 |\n| `mdLink` | 링크 텍스트 |\n| `mdLinkUrl` | 링크 URL |\n| `mdCode` | 인라인 코드 |\n| `mdCodeBlock` | 코드 블록 내용 |\n| `mdCodeBlockBorder` | 코드 블록 울타리 |\n| `mdQuote` | 인용문 텍스트 |\n| `mdQuoteBorder` | 인용구 테두리 |\n| `mdHr` | 수평선 |\n| `mdListBullet` | 글머리 기호 나열 |\n\n### 도구 차이점(3가지 색상)\n\n| 토큰 | 목적 |\n|-------|---------|\n| `toolDiffAdded` | 라인 추가 |\n| `toolDiffRemoved` | 제거된 줄 |\n| `toolDiffContext` | 컨텍스트 라인 |\n\n### 구문 강조(9가지 색상)\n\n| 토큰 | 목적 |\n|-------|---------|\n| `syntaxComment` | 댓글 |\n| `syntaxKeyword` | 키워드 |\n| `syntaxFunction` | 함수 이름 |\n| `syntaxVariable` | 변수 |\n| `syntaxString` | 문자열 |\n| `syntaxNumber` | 숫자 |\n| `syntaxType` | 유형 |\n| `syntaxOperator` | 연산자 |\n| `syntaxPunctuation` | 구두 |\n\n### 사고 수준 경계(6개 필수, 1개 선택)\n\n사고 수준을 나타내는 편집기 테두리 색상(미묘한 수준에서 눈에 띄는 수준까지의 시각적 계층 구조):\n\n| 토큰 | 목적 |\n|-------|---------|\n| `thinkingOff` | 생각하다 |\n| `thinkingMinimal` | 최소한의 사고 |\n| `thinkingLow` | 낮은 사고 |\n| `thinkingMedium` | 중간 사고 |\n| `thinkingHigh` | 높은 사고 |\n| `thinkingXhigh` | 매우 높은 사고 |\n| `thinkingMax` | 최대 사고; 선택사항, `thinkingXhigh`로 대체 |\n\n### 배쉬 모드(1색)\n\n| 토큰 | 목적 |\n|-------|---------|\n| `bashMode` | bash 모드의 편집기 테두리(`!` 접두사) |\n\n### HTML 내보내기(선택 사항)\n\n`export` 섹션은 `/export` HTML 출력의 색상을 제어합니다. 생략하면 색상은 `userMessageBg`에서 파생됩니다.\n\n```json\n{\n  \"export\": {\n    \"pageBg\": \"#18181e\",\n    \"cardBg\": \"#1e1e24\",\n    \"infoBg\": \"#3c3728\"\n  }\n}\n```\n\n## 색상 값\n\n네 가지 형식이 지원됩니다.\n\n| 체재 | 예 | 설명 |\n|--------|---------|-------------|\n| 마녀 | `\"#ff0000\"` | 6자리 16진수 RGB |\n| 256색 | `39` | xterm 256색 팔레트 색인(0-255) |\n| 변하기 쉬운 | `\"primary\"` | `vars` 항목에 대한 참조 |\n| 기본 | `\"\"` | 터미널의 기본 색상 |\n\n### 256색 팔레트\n\n- `0-15`: 기본 ANSI 색상(터미널에 따라 다름)\n- `16-231`: 6×6×6 RGB 큐브(`16 + 36×R + 6×G + B` 여기서 R,G,B는 0-5)\n- `232-255`: 그레이스케일 램프\n\n### 터미널 호환성\n\nPi 24비트 RGB 색상을 사용합니다. 대부분의 최신 터미널은 이를 지원합니다(iTerm2, Kitty, WezTerm, Windows Terminal, VS Code). 256색만 지원하는 구형 터미널의 경우 pi는 가장 가까운 근사값으로 돌아갑니다.\n\n트루컬러 지원을 확인하세요.\n\n```bash\necho $COLORTERM  # Should output \"truecolor\" or \"24bit\"\n```\n\n## 팁\n\n**어두운 터미널:** 더 높은 대비와 함께 밝고 채도가 높은 색상을 사용합니다.\n\n**밝은 터미널:** 대비가 낮고 더 어둡고 차분한 색상을 사용합니다.\n\n**색상 조화:** 기본 팔레트(Nord, Gruvbox, Tokyo Night)로 시작하여 `vars`에서 정의하고 일관되게 참조합니다.\n\n**테스트:** 다양한 메시지 유형, 도구 상태, 마크다운 콘텐츠, 길게 줄 바꿈된 텍스트로 테마를 확인하세요.\n\n**VS 코드:** 정확한 색상을 위해 `terminal.integrated.minimumContrastRatio`에서 `1`로 설정하세요.\n\n## 예\n\n내장된 테마를 확인하세요:\n- [dark.json](../src/modes/interactive/theme/dark.json)\n- [light.json](../src/modes/interactive/theme/light.json)","sourceFile":"themes.md"},"tmux":{"title":"tmux 설정","markdown":"Pi는 tmux 내부에서 작동하지만 tmux는 기본적으로 특정 키에서 수정자 정보를 제거합니다. 구성하지 않으면 `Shift+Enter` 및 `Ctrl+Enter`는 일반적으로 일반 `Enter`와 구별할 수 없습니다.\n\n## 권장 구성\n\n`~/.tmux.conf`에 추가:\n\n```tmux\nset -g extended-keys on\nset -g extended-keys-format csi-u\n```\n\n그런 다음 tmux를 완전히 다시 시작하세요.\n\n```bash\ntmux kill-server\ntmux\n```\n\nPi 키티 키보드 프로토콜을 사용할 수 없을 때 자동으로 확장 키 보고를 요청합니다. `extended-keys-format csi-u`를 사용하면 tmux는 가장 안정적인 구성인 CSI-u 형식으로 수정된 키를 전달합니다. `extended-keys-format` 옵션을 사용하려면 tmux 3.5 이상이 필요합니다.\n\n## `csi-u`이 권장되는 이유\n\n다음만 포함:\n\n```tmux\nset -g extended-keys on\n```\n\ntmux의 기본값은 `extended-keys-format xterm`입니다. 애플리케이션이 확장 키 보고를 요청하면 수정된 키는 다음과 같은 xterm `modifyOtherKeys` 형식으로 전달됩니다.\n\n- `Ctrl+C` → `\\x1b[27;5;99~`\n- `Ctrl+D` → `\\x1b[27;5;100~`\n- `Ctrl+Enter` → `\\x1b[27;5;13~`\n\n`extended-keys-format csi-u`를 사용하면 동일한 키가 다음과 같이 전달됩니다.\n\n- `Ctrl+C` → `\\x1b[99;5u`\n- `Ctrl+D` → `\\x1b[100;5u`\n- `Ctrl+Enter` → `\\x1b[13;5u`\n\nPi는 두 가지 형식을 모두 지원하지만 `csi-u`가 권장되는 tmux 설정입니다.\n\n## 이로 인해 해결되는 사항\n\ntmux 확장 키가 없으면 수정된 Enter 키가 레거시 시퀀스로 축소됩니다.\n\n| 열쇠 | 확장키 없이 | `csi-u`로 |\n|-----|-----------------|--------------|\n| 입력하다 | `\\r` | `\\r` |\n| Shift+Enter | `\\r` | `\\x1b[13;2u` |\n| Ctrl+Enter | `\\r` | `\\x1b[13;5u` |\n| Alt/Option+Enter | `\\x1b\\r` | `\\x1b[13;3u` |\n\n이는 기본 키 바인딩(제출의 경우 `Enter`, 개행의 경우 `Shift+Enter`)과 수정된 Enter를 사용하는 모든 사용자 지정 키 바인딩에 영향을 미칩니다.\n\n## 요구사항\n\n- tmux `extended-keys-format csi-u`의 경우 3.5 이상(확인하려면 `tmux -V`을 실행하세요)\n- 확장 키를 지원하는 터미널 에뮬레이터(Ghostty, Kitty, iTerm2, WezTerm, Windows Terminal)\n\ntmux 3.2~3.4에서는 `extended-keys-format csi-u`를 생략합니다. Pi는 여전히 tmux의 기본 xterm `modifyOtherKeys` 형식을 지원합니다.","sourceFile":"tmux.md"},"tui":{"title":"TUI 구성품","markdown":"> pi는 TUI 구성요소를 생성할 수 있습니다. 귀하의 사용 사례에 맞게 구축하도록 요청하세요.\n\n\nExtensions 및 맞춤 도구는 대화형 사용자 인터페이스를 위한 맞춤 TUI 구성 요소를 렌더링할 수 있습니다. 이 페이지에서는 구성 요소 시스템과 사용 가능한 빌딩 블록을 다룹니다.\n\n**출처:** [`@earendil-works/pi-tui`](https://github.com/earendil-works/pi-mono/tree/main/packages/tui)\n\n## 구성 요소 인터페이스\n\n모든 구성요소는 다음을 구현합니다.\n\n```typescript\ninterface Component {\n  render(width: number): string[];\n  handleInput?(data: string): void;\n  wantsKeyRelease?: boolean;\n  invalidate(): void;\n}\n```\n\n| 방법 | 설명 |\n|--------|-------------|\n| `render(width)` | 문자열 배열을 반환합니다(한 줄에 하나씩). 각 줄은 **`width`**을 초과할 수 없습니다. |\n| `handleInput?(data)` | 구성 요소에 포커스가 있을 때 키보드 입력을 받습니다. |\n| `wantsKeyRelease?` | true인 경우 구성요소는 키 릴리스 이벤트(Kitty 프로토콜)를 수신합니다. 기본값: 거짓. |\n| `invalidate()` | 캐시된 렌더링 상태를 지웁니다. 테마 변경 시 호출됩니다. |\n\nTUI는 렌더링된 각 줄의 끝에 전체 SGR 재설정과 OSC 8 재설정을 추가합니다. 스타일은 줄을 넘어 전달되지 않습니다. 스타일을 사용하여 여러 줄로 된 텍스트를 내보내는 경우 줄당 스타일을 다시 적용하거나 `wrapTextWithAnsi()`를 사용하여 줄바꿈된 각 줄에 대해 스타일이 유지되도록 합니다.\n\n## 포커스 가능 인터페이스(IME 지원)\n\n텍스트 커서를 표시하고 IME(입력 방법 편집기) 지원이 필요한 구성 요소는 `Focusable` 인터페이스를 구현해야 합니다.\n\n```typescript\nimport { CURSOR_MARKER, type Component, type Focusable } from \"@earendil-works/pi-tui\";\n\nclass MyInput implements Component, Focusable {\n  focused: boolean = false;  // Set by TUI when focus changes\n  \n  render(width: number): string[] {\n    const marker = this.focused ? CURSOR_MARKER : \"\";\n    // Emit marker right before the fake cursor\n    return [`> ${beforeCursor}${marker}\\x1b[7m${atCursor}\\x1b[27m${afterCursor}`];\n  }\n}\n```\n\n`Focusable` 구성 요소에 포커스가 있는 경우 TUI:\n1. 구성요소에 `focused = true`를 설정합니다.\n2. `CURSOR_MARKER`(너비가 0인 APC 이스케이프 시퀀스)에 대해 렌더링된 출력을 검사합니다.\n3. 해당 위치에 하드웨어 터미널 커서를 배치합니다.\n4. `showHardwareCursor`가 활성화된 경우에만 하드웨어 커서를 표시합니다.\n\n커서는 기본적으로 숨겨져 있습니다. 이렇게 하면 숨겨진 커서가 있는 IME 후보 창을 추적하는 터미널에 대한 하드웨어 커서의 위치를 ​​계속 지정하면서 가짜 커서 렌더링이 유지됩니다. 일부 터미널에는 IME 위치 지정을 위해 눈에 보이는 하드웨어 커서가 필요합니다. `showHardwareCursor`, `setShowHardwareCursor(true)` 또는 `PI_HARDWARE_CURSOR=1`로 활성화하세요. `Editor` 및 `Input` 내장 구성요소는 이미 이 인터페이스를 구현합니다.\n\n### 입력이 내장된 컨테이너 구성 요소\n\n컨테이너 구성 요소(대화 상자, 선택기 등)에 `Input` 또는 `Editor` 하위 항목이 포함된 경우 컨테이너는 `Focusable`를 구현하고 포커스 상태를 하위 항목에 전파해야 합니다. 그렇지 않으면 하드웨어 커서가 IME 입력에 대해 올바르게 배치되지 않습니다.\n\n```typescript\nimport { Container, type Focusable, Input } from \"@earendil-works/pi-tui\";\n\nclass SearchDialog extends Container implements Focusable {\n  private searchInput: Input;\n\n  // Focusable implementation - propagate to child input for IME cursor positioning\n  private _focused = false;\n  get focused(): boolean {\n    return this._focused;\n  }\n  set focused(value: boolean) {\n    this._focused = value;\n    this.searchInput.focused = value;\n  }\n\n  constructor() {\n    super();\n    this.searchInput = new Input();\n    this.addChild(this.searchInput);\n  }\n}\n```\n\n이러한 전파가 없으면 IME(중국어, 일본어, 한국어 등)를 사용하여 입력하면 화면의 잘못된 위치에 후보 창이 표시됩니다.\n\n## 구성요소 사용\n\n**확장 프로그램**에서 `ctx.ui.custom()`을 통해:\n\n```typescript\npi.on(\"session_start\", async (_event, ctx) => {\n  const result = await ctx.ui.custom<string | null>((tui, theme, keybindings, done) =>\n    new MyComponent({\n      theme,\n      keybindings,\n      onChange: () => tui.requestRender(),\n      onSelect: (value) => done(value),\n      onCancel: () => done(null),\n    })\n  );\n});\n```\n\n**맞춤 도구**에서 `ctx.ui.custom()`를 통해:\n\n```typescript\nasync execute(toolCallId, params, signal, onUpdate, ctx) {\n  const result = await ctx.ui.custom<string | null>((tui, theme, keybindings, done) =>\n    new MyComponent({\n      theme,\n      keybindings,\n      onChange: () => tui.requestRender(),\n      onSelect: (value) => done(value),\n      onCancel: () => done(null),\n    })\n  );\n  // Use result...\n}\n```\n\n## 오버레이\n\n화면을 지우지 않고 기존 콘텐츠 위에 렌더링 구성 요소를 오버레이합니다. `{ overlay: true }`에서 `ctx.ui.custom()`로 전달:\n\n```typescript\nconst result = await ctx.ui.custom<string | null>(\n  (tui, theme, keybindings, done) => new MyDialog({ onClose: done }),\n  { overlay: true }\n);\n```\n\n위치 지정 및 크기 조정에는 `overlayOptions`를 사용하세요.\n\n```typescript\nconst result = await ctx.ui.custom<string | null>(\n  (tui, theme, keybindings, done) => new SidePanel({ onClose: done }),\n  {\n    overlay: true,\n    overlayOptions: {\n      // Size: number or percentage string\n      width: \"50%\",          // 50% of terminal width\n      minWidth: 40,          // minimum 40 columns\n      maxHeight: \"80%\",      // max 80% of terminal height\n\n      // Position: anchor-based (default: \"center\")\n      anchor: \"right-center\", // 9 positions: center, top-left, top-center, etc.\n      offsetX: -2,            // offset from anchor\n      offsetY: 0,\n\n      // Or percentage/absolute positioning\n      row: \"25%\",            // 25% from top\n      col: 10,               // column 10\n\n      // Margins\n      margin: 2,             // all sides, or { top, right, bottom, left }\n\n      // Responsive: hide on narrow terminals\n      visible: (termWidth, termHeight) => termWidth >= 80,\n    },\n    // Get handle for programmatic focus and visibility control\n    onHandle: (handle) => {\n      // handle.focus() - focus this overlay and bring it to the visual front\n      // handle.unfocus() - release input to normal fallback\n      // handle.unfocus({ target }) - release input to a specific component or null\n      // handle.setHidden(true/false) - toggle visibility\n      // handle.hide() - permanently remove\n    },\n  }\n);\n```\n\n### 오버레이 포커스\n\n집중된 가시 오버레이는 오버레이가 아닌 임시 UI 전반에 걸쳐 입력 소유권을 유지합니다. 오버레이가 `{ overlay: true }` 없이 다른 `ctx.ui.custom()` 구성 요소를 열면 해당 대체 UI가 활성화된 동안 입력을 받습니다. 닫히면 초점이 맞춰진 오버레이가 입력을 회수할 수 있습니다.\n\n표시 오버레이가 입력 소유를 중지해야 하는 경우 `handle.unfocus()`를 사용하고 TUI가 다른 표시 캡처 오버레이 또는 이전 포커스 대상으로 대체되도록 합니다. 오버레이가 계속 표시되는 동안 특정 구성요소가 입력을 받아야 하는 경우 `handle.unfocus({ target })`를 사용하세요. `{ target: null }`를 전달하면 포커스가 다시 설정될 때까지 의도적으로 포커스된 구성 요소가 남지 ​​않습니다.\n\n### 오버레이 수명주기\n\n오버레이 구성요소는 닫히면 삭제됩니다. 참조를 재사용하지 마세요 - 새로운 인스턴스를 만드세요:\n\n```typescript\n// Wrong - stale reference\nlet menu: MenuComponent;\nawait ctx.ui.custom((_, __, ___, done) => {\n  menu = new MenuComponent(done);\n  return menu;\n}, { overlay: true });\nsetActiveComponent(menu);  // Disposed\n\n// Correct - re-call to re-show\nconst showMenu = () => ctx.ui.custom((_, __, ___, done) => \n  new MenuComponent(done), { overlay: true });\n\nawait showMenu();  // First show\nawait showMenu();  // \"Back\" = just call again\n```\n\n앵커, 여백, 스태킹, 반응형 가시성 및 애니메이션을 다루는 포괄적인 예는 [overlay-qa-tests.ts](../examples/extensions/overlay-qa-tests.ts)를 참조하세요.\n\n## 내장 구성요소\n\n`@earendil-works/pi-tui`에서 가져오기:\n\n```typescript\nimport { Text, Box, Container, Spacer, Markdown } from \"@earendil-works/pi-tui\";\n```\n\n### 텍스트\n\n단어 줄바꿈이 포함된 여러 줄의 텍스트입니다.\n\n```typescript\nconst text = new Text(\n  \"Hello World\",    // content\n  1,                // paddingX (default: 1)\n  1,                // paddingY (default: 1)\n  (s) => bgGray(s)  // optional background function\n);\ntext.setText(\"Updated\");\n```\n\n### 상자\n\n패딩과 배경색이 포함된 컨테이너입니다.\n\n```typescript\nconst box = new Box(\n  1,                // paddingX\n  1,                // paddingY\n  (s) => bgGray(s)  // background function\n);\nbox.addChild(new Text(\"Content\", 0, 0));\nbox.setBgFn((s) => bgBlue(s));\n```\n\n### 컨테이너\n\n하위 구성요소를 수직으로 그룹화합니다.\n\n```typescript\nconst container = new Container();\ncontainer.addChild(component1);\ncontainer.addChild(component2);\ncontainer.removeChild(component1);\n```\n\n### 스페이서\n\n빈 수직 공간.\n\n```typescript\nconst spacer = new Spacer(2);  // 2 empty lines\n```\n\n### Markdown\n\n구문 강조를 사용하여 마크다운을 렌더링합니다.\n\n```typescript\nconst md = new Markdown(\n  \"# Title\\n\\nSome **bold** text\",\n  1,        // paddingX\n  1,        // paddingY\n  theme     // MarkdownTheme (see below)\n);\nmd.setText(\"Updated markdown\");\n```\n\n### 영상\n\n지원되는 터미널(Kitty, iTerm2, Ghostty, WezTerm, Warp)에서 이미지를 렌더링합니다.\n\n```typescript\nconst image = new Image(\n  base64Data,   // base64-encoded image\n  \"image/png\",  // MIME type\n  theme,        // ImageTheme\n  { maxWidthCells: 80, maxHeightCells: 24 }\n);\n```\n\n## 키보드 입력\n\n키 감지에는 `matchesKey()`를 사용하세요.\n\n```typescript\nimport { matchesKey, Key } from \"@earendil-works/pi-tui\";\n\nhandleInput(data: string) {\n  if (matchesKey(data, Key.up)) {\n    this.selectedIndex--;\n  } else if (matchesKey(data, Key.enter)) {\n    this.onSelect?.(this.selectedIndex);\n  } else if (matchesKey(data, Key.escape)) {\n    this.onCancel?.();\n  } else if (matchesKey(data, Key.ctrl(\"c\"))) {\n    // Ctrl+C\n  }\n}\n```\n\n**주요 식별자**(자동 완성 또는 문자열 리터럴의 경우 `Key.*` 사용):\n- 기본 키: `Key.enter`, `Key.escape`, `Key.tab`, `Key.space`, `Key.backspace`, `Key.delete`, `Key.home`, `Key.end`\n- 화살표 키: `Key.up`, `Key.down`, `Key.left`, `Key.right`\n- 수정자 포함: `Key.ctrl(\"c\")`, `Key.shift(\"tab\")`, `Key.alt(\"left\")`, `Key.ctrlShift(\"p\")`\n- 문자열 형식도 작동합니다: `\"enter\"`, `\"ctrl+c\"`, `\"shift+tab\"`, `\"ctrl+shift+p\"`\n\n## 선 너비\n\n**필수:** `render()`의 각 줄은 `width` 매개변수를 초과해서는 안 됩니다.\n\n```typescript\nimport { visibleWidth, truncateToWidth } from \"@earendil-works/pi-tui\";\n\nrender(width: number): string[] {\n  // Truncate long lines\n  return [truncateToWidth(this.text, width)];\n}\n```\n\n유용:\n- `visibleWidth(str)` - 디스플레이 너비 가져오기(ANSI 코드 무시)\n- `truncateToWidth(str, width, ellipsis?)` - 선택적 줄임표로 자르기\n- `wrapTextWithAnsi(str, width)` - ANSI 코드를 보존하는 단어 줄 바꿈\n\n## 사용자 정의 구성 요소 만들기\n\n예: 대화형 선택기\n\n```typescript\nimport {\n  matchesKey, Key,\n  truncateToWidth, visibleWidth\n} from \"@earendil-works/pi-tui\";\n\nclass MySelector {\n  private items: string[];\n  private selected = 0;\n  private cachedWidth?: number;\n  private cachedLines?: string[];\n  \n  public onSelect?: (item: string) => void;\n  public onCancel?: () => void;\n\n  constructor(items: string[]) {\n    this.items = items;\n  }\n\n  handleInput(data: string): void {\n    if (matchesKey(data, Key.up) && this.selected > 0) {\n      this.selected--;\n      this.invalidate();\n    } else if (matchesKey(data, Key.down) && this.selected < this.items.length - 1) {\n      this.selected++;\n      this.invalidate();\n    } else if (matchesKey(data, Key.enter)) {\n      this.onSelect?.(this.items[this.selected]);\n    } else if (matchesKey(data, Key.escape)) {\n      this.onCancel?.();\n    }\n  }\n\n  render(width: number): string[] {\n    if (this.cachedLines && this.cachedWidth === width) {\n      return this.cachedLines;\n    }\n\n    this.cachedLines = this.items.map((item, i) => {\n      const prefix = i === this.selected ? \"> \" : \"  \";\n      return truncateToWidth(prefix + item, width);\n    });\n    this.cachedWidth = width;\n    return this.cachedLines;\n  }\n\n  invalidate(): void {\n    this.cachedWidth = undefined;\n    this.cachedLines = undefined;\n  }\n}\n```\n\n확장에서의 사용법:\n\n```typescript\npi.registerCommand(\"pick\", {\n  description: \"Pick an item\",\n  handler: async (_args, ctx) => {\n    const items = [\"Option A\", \"Option B\", \"Option C\"];\n    const selected = await ctx.ui.custom<string | null>((tui, _theme, _keybindings, done) => {\n      const selector = new MySelector(items);\n      selector.onSelect = done;\n      selector.onCancel = () => done(null);\n\n      return {\n        render: (width) => selector.render(width),\n        handleInput: (data) => {\n          selector.handleInput(data);\n          tui.requestRender();\n        },\n        invalidate: () => selector.invalidate(),\n      };\n    });\n\n    if (selected !== null) {\n      ctx.ui.notify(`Selected: ${selected}`, \"info\");\n    }\n  }\n});\n```\n\n## 테마\n\n구성 요소는 스타일 지정을 위해 테마 개체를 허용합니다.\n\n**`renderCall`/`renderResult`**에서는 `theme` 매개변수를 사용합니다.\n\n```typescript\nrenderResult(result, options, theme, context) {\n  // Use theme.fg() for foreground colors\n  return new Text(theme.fg(\"success\", \"Done!\"), 0, 0);\n  \n  // Use theme.bg() for background colors\n  const styled = theme.bg(\"toolPendingBg\", theme.fg(\"accent\", \"text\"));\n}\n```\n\n**전경 색상**(`theme.fg(color, text)`):\n\n| 범주 | 그림 물감 |\n|----------|--------|\n| 일반적인 | `text`, `accent`, `muted`, `dim` |\n| 상태 | `success`, `error`, `warning` |\n| 테두리 | `border`, `borderAccent`, `borderMuted` |\n| 메시지 | `userMessageText`, `customMessageText`, `customMessageLabel` |\n| 도구 | `toolTitle`, `toolOutput` |\n| 차이점 | `toolDiffAdded`, `toolDiffRemoved`, `toolDiffContext` |\n| Markdown | `mdHeading`, `mdLink`, `mdLinkUrl`, `mdCode`, `mdCodeBlock`, `mdCodeBlockBorder`, `mdQuote`, `mdQuoteBorder`, `mdHr`, `mdListBullet` |\n| 통사론 | `syntaxComment`, `syntaxKeyword`, `syntaxFunction`, `syntaxVariable`, `syntaxString`, `syntaxNumber`, `syntaxType`, `syntaxOperator`, `syntaxPunctuation` |\n| 생각 | `thinkingOff`, `thinkingMinimal`, `thinkingLow`, `thinkingMedium`, `thinkingHigh`, `thinkingXhigh`, `thinkingMax` |\n| 모드 | `bashMode` |\n\n**배경 색상**(`theme.bg(color, text)`):\n\n`selectedBg`, `userMessageBg`, `customMessageBg`, `toolPendingBg`, `toolSuccessBg`, `toolErrorBg`\n\n**Markdown**의 경우 `getMarkdownTheme()`를 사용하세요.\n\n```typescript\nimport { getMarkdownTheme } from \"@earendil-works/pi-coding-agent\";\nimport { Markdown } from \"@earendil-works/pi-tui\";\n\nrenderResult(result, options, theme, context) {\n  const mdTheme = getMarkdownTheme();\n  return new Markdown(result.details.markdown, 0, 0, mdTheme);\n}\n```\n\n**맞춤 구성요소**의 경우 자체 테마 인터페이스를 정의하세요.\n\n```typescript\ninterface MyTheme {\n  selected: (s: string) => string;\n  normal: (s: string) => string;\n}\n```\n\n## 디버그 로깅\n\n`PI_TUI_WRITE_LOG`를 설정하여 stdout에 기록된 원시 ANSI 스트림을 캡처합니다.\n\n```bash\nPI_TUI_WRITE_LOG=/tmp/tui-ansi.log npx tsx packages/tui/test/chat-simple.ts\n```\n\n## 성능\n\n가능한 경우 렌더링된 출력을 캐시합니다.\n\n```typescript\nclass CachedComponent {\n  private cachedWidth?: number;\n  private cachedLines?: string[];\n\n  render(width: number): string[] {\n    if (this.cachedLines && this.cachedWidth === width) {\n      return this.cachedLines;\n    }\n    // ... compute lines ...\n    this.cachedWidth = width;\n    this.cachedLines = lines;\n    return lines;\n  }\n\n  invalidate(): void {\n    this.cachedWidth = undefined;\n    this.cachedLines = undefined;\n  }\n}\n```\n\n상태가 변경되면 `invalidate()`를 호출한 다음 삽입된 `tui.requestRender()`를 사용하여 다시 렌더링을 트리거합니다.\n\n## 무효화 및 테마 변경\n\n테마가 변경되면 TUI는 모든 구성 요소에서 `invalidate()`를 호출하여 캐시를 지웁니다. 테마 변경사항이 적용되도록 구성요소는 `invalidate()`를 올바르게 구현해야 합니다.\n\n### 문제\n\n구성 요소가 테마 색상을 문자열(`theme.fg()`, `theme.bg()` 등을 통해)로 미리 굽고 캐시하는 경우 캐시된 문자열에는 이전 테마의 ANSI 이스케이프 코드가 포함됩니다. 구성 요소가 테마 콘텐츠를 별도로 저장하는 경우 단순히 렌더링 캐시를 지우는 것만으로는 충분하지 않습니다.\n\n**잘못된 접근 방식**(테마 색상이 업데이트되지 않음):\n\n```typescript\nclass BadComponent extends Container {\n  private content: Text;\n\n  constructor(message: string, theme: Theme) {\n    super();\n    // Pre-baked theme colors stored in Text component\n    this.content = new Text(theme.fg(\"accent\", message), 1, 0);\n    this.addChild(this.content);\n  }\n  // No invalidate override - parent's invalidate only clears\n  // child render caches, not the pre-baked content\n}\n```\n\n### 해결책\n\n테마 색상으로 콘텐츠를 빌드하는 구성 요소는 `invalidate()`가 호출될 때 해당 콘텐츠를 다시 빌드해야 합니다.\n\n```typescript\nclass GoodComponent extends Container {\n  private message: string;\n  private content: Text;\n\n  constructor(message: string) {\n    super();\n    this.message = message;\n    this.content = new Text(\"\", 1, 0);\n    this.addChild(this.content);\n    this.updateDisplay();\n  }\n\n  private updateDisplay(): void {\n    // Rebuild content with current theme\n    this.content.setText(theme.fg(\"accent\", this.message));\n  }\n\n  override invalidate(): void {\n    super.invalidate();  // Clear child caches\n    this.updateDisplay(); // Rebuild with new theme\n  }\n}\n```\n\n### 패턴: 무효화 시 재구축\n\n복잡한 콘텐츠가 포함된 구성요소의 경우:\n\n```typescript\nclass ComplexComponent extends Container {\n  private data: SomeData;\n\n  constructor(data: SomeData) {\n    super();\n    this.data = data;\n    this.rebuild();\n  }\n\n  private rebuild(): void {\n    this.clear();  // Remove all children\n\n    // Build UI with current theme\n    this.addChild(new Text(theme.fg(\"accent\", theme.bold(\"Title\")), 1, 0));\n    this.addChild(new Spacer(1));\n\n    for (const item of this.data.items) {\n      const color = item.active ? \"success\" : \"muted\";\n      this.addChild(new Text(theme.fg(color, item.label), 1, 0));\n    }\n  }\n\n  override invalidate(): void {\n    super.invalidate();\n    this.rebuild();\n  }\n}\n```\n\n### 이것이 중요한 경우\n\n이 패턴은 다음과 같은 경우에 필요합니다.\n\n1. **사전 베이킹 테마 색상** - `theme.fg()` 또는 `theme.bg()`를 사용하여 하위 구성 요소에 저장된 스타일 문자열 생성\n2. **구문 강조** - 테마 기반 구문 색상을 적용하는 `highlightCode()` 사용\n3. **복잡한 레이아웃** - 테마 색상이 포함된 하위 구성요소 트리 구축\n\n다음과 같은 경우에는 이 패턴이 필요하지 않습니다.\n\n1. **테마 콜백 사용** - 렌더링 중에 호출되는 `(text) => theme.fg(\"accent\", text)`와 같은 함수 전달\n2. **간단한 컨테이너** - 테마 콘텐츠를 추가하지 않고 다른 구성요소를 그룹화하기만 하면 됩니다.\n3. **상태 비저장 렌더링** - 모든 `render()` 호출에서 테마 출력을 새로 계산합니다(캐싱 없음).\n\n## 일반적인 패턴\n\n이러한 패턴은 확장에서 가장 일반적인 UI 요구 사항을 다룹니다. **처음부터 만드는 대신 이 패턴을 복사하세요.**\n\n### 패턴 1: 선택 대화 상자(SelectList)\n\n사용자가 옵션 목록에서 선택할 수 있도록 합니다. 프레이밍을 위해 `@earendil-works/pi-tui`에서 `SelectList`를 `DynamicBorder`와 함께 사용하세요.\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { DynamicBorder } from \"@earendil-works/pi-coding-agent\";\nimport { Container, type SelectItem, SelectList, Text } from \"@earendil-works/pi-tui\";\n\npi.registerCommand(\"pick\", {\n  handler: async (_args, ctx) => {\n    const items: SelectItem[] = [\n      { value: \"opt1\", label: \"Option 1\", description: \"First option\" },\n      { value: \"opt2\", label: \"Option 2\", description: \"Second option\" },\n      { value: \"opt3\", label: \"Option 3\" },  // description is optional\n    ];\n\n    const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {\n      const container = new Container();\n\n      // Top border\n      container.addChild(new DynamicBorder((s: string) => theme.fg(\"accent\", s)));\n\n      // Title\n      container.addChild(new Text(theme.fg(\"accent\", theme.bold(\"Pick an Option\")), 1, 0));\n\n      // SelectList with theme\n      const selectList = new SelectList(items, Math.min(items.length, 10), {\n        selectedPrefix: (t) => theme.fg(\"accent\", t),\n        selectedText: (t) => theme.fg(\"accent\", t),\n        description: (t) => theme.fg(\"muted\", t),\n        scrollInfo: (t) => theme.fg(\"dim\", t),\n        noMatch: (t) => theme.fg(\"warning\", t),\n      });\n      selectList.onSelect = (item) => done(item.value);\n      selectList.onCancel = () => done(null);\n      container.addChild(selectList);\n\n      // Help text\n      container.addChild(new Text(theme.fg(\"dim\", \"↑↓ navigate • enter select • esc cancel\"), 1, 0));\n\n      // Bottom border\n      container.addChild(new DynamicBorder((s: string) => theme.fg(\"accent\", s)));\n\n      return {\n        render: (w) => container.render(w),\n        invalidate: () => container.invalidate(),\n        handleInput: (data) => { selectList.handleInput(data); tui.requestRender(); },\n      };\n    });\n\n    if (result) {\n      ctx.ui.notify(`Selected: ${result}`, \"info\");\n    }\n  },\n});\n```\n\n**예:** [preset.ts](../examples/extensions/preset.ts), [tools.ts](../examples/extensions/tools.ts)\n\n### 패턴 2: 취소를 사용한 비동기 작업(BorderedLoader)\n\n시간이 걸리고 취소 가능해야 하는 작업의 경우. `BorderedLoader`는 스피너를 표시하고 취소를 위한 이스케이프를 처리합니다.\n\n```typescript\nimport { BorderedLoader } from \"@earendil-works/pi-coding-agent\";\n\npi.registerCommand(\"fetch\", {\n  handler: async (_args, ctx) => {\n    const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {\n      const loader = new BorderedLoader(tui, theme, \"Fetching data...\");\n      loader.onAbort = () => done(null);\n\n      // Do async work\n      fetchData(loader.signal)\n        .then((data) => done(data))\n        .catch(() => done(null));\n\n      return loader;\n    });\n\n    if (result === null) {\n      ctx.ui.notify(\"Cancelled\", \"info\");\n    } else {\n      ctx.ui.setEditorText(result);\n    }\n  },\n});\n```\n\n**예:** [qna.ts](../examples/extensions/qna.ts), [handoff.ts](../examples/extensions/handoff.ts)\n\n### 패턴 3: 설정/토글(SettingsList)\n\n여러 설정을 전환합니다. `getSettingsListTheme()`와 함께 `@earendil-works/pi-tui`의 `SettingsList`를 사용하세요.\n\n```typescript\nimport { getSettingsListTheme } from \"@earendil-works/pi-coding-agent\";\nimport { Container, type SettingItem, SettingsList, Text } from \"@earendil-works/pi-tui\";\n\npi.registerCommand(\"settings\", {\n  handler: async (_args, ctx) => {\n    const items: SettingItem[] = [\n      { id: \"verbose\", label: \"Verbose mode\", currentValue: \"off\", values: [\"on\", \"off\"] },\n      { id: \"color\", label: \"Color output\", currentValue: \"on\", values: [\"on\", \"off\"] },\n    ];\n\n    await ctx.ui.custom((_tui, theme, _kb, done) => {\n      const container = new Container();\n      container.addChild(new Text(theme.fg(\"accent\", theme.bold(\"Settings\")), 1, 1));\n\n      const settingsList = new SettingsList(\n        items,\n        Math.min(items.length + 2, 15),\n        getSettingsListTheme(),\n        (id, newValue) => {\n          // Handle value change\n          ctx.ui.notify(`${id} = ${newValue}`, \"info\");\n        },\n        () => done(undefined),  // On close\n        { enableSearch: true }, // Optional: enable fuzzy search by label\n      );\n      container.addChild(settingsList);\n\n      return {\n        render: (w) => container.render(w),\n        invalidate: () => container.invalidate(),\n        handleInput: (data) => settingsList.handleInput?.(data),\n      };\n    });\n  },\n});\n```\n\n**예:** [tools.ts](../examples/extensions/tools.ts)\n\n### 패턴 4: 지속적인 상태 표시기\n\n렌더링 전반에 걸쳐 지속되는 바닥글에 상태를 표시합니다. 모드 표시기에 적합합니다.\n\n```typescript\n// Set status (shown in footer)\nctx.ui.setStatus(\"my-ext\", ctx.ui.theme.fg(\"accent\", \"● active\"));\n\n// Clear status\nctx.ui.setStatus(\"my-ext\", undefined);\n```\n\n**예:** [status-line.ts](../examples/extensions/status-line.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts)\n\n### 패턴 4b: 작업 표시기 사용자 정의\n\npi가 응답을 스트리밍하는 동안 표시되는 인라인 작업 표시기를 사용자 정의합니다.\n\n```typescript\n// Static indicator\nctx.ui.setWorkingIndicator({ frames: [ctx.ui.theme.fg(\"accent\", \"●\")] });\n\n// Custom animated indicator\nctx.ui.setWorkingIndicator({\n  frames: [\n    ctx.ui.theme.fg(\"dim\", \"·\"),\n    ctx.ui.theme.fg(\"muted\", \"•\"),\n    ctx.ui.theme.fg(\"accent\", \"●\"),\n    ctx.ui.theme.fg(\"muted\", \"•\"),\n  ],\n  intervalMs: 120,\n});\n\n// Hide the indicator entirely\nctx.ui.setWorkingIndicator({ frames: [] });\n\n// Restore pi's default spinner\nctx.ui.setWorkingIndicator();\n```\n\n이는 일반 스트리밍 작동 표시에만 영향을 미칩니다. 압축 및 재시도 로더는 기본 제공 스타일을 유지합니다. 사용자 정의 프레임은 그대로 렌더링되므로 확장 프로그램은 필요할 때 자체 색상을 추가해야 합니다.\n\n**예:** [working-indicator.ts](../examples/extensions/working-indicator.ts)\n\n### 패턴 5: 편집기 위/아래 위젯\n\n입력 편집기 위 또는 아래에 영구 콘텐츠를 표시합니다. 할 일 목록, 진행에 좋습니다.\n\n```typescript\n// Simple string array (above editor by default)\nctx.ui.setWidget(\"my-widget\", [\"Line 1\", \"Line 2\"]);\n\n// Render below the editor\nctx.ui.setWidget(\"my-widget\", [\"Line 1\", \"Line 2\"], { placement: \"belowEditor\" });\n\n// Or with theme\nctx.ui.setWidget(\"my-widget\", (_tui, theme) => {\n  const lines = items.map((item, i) =>\n    item.done\n      ? theme.fg(\"success\", \"✓ \") + theme.fg(\"muted\", item.text)\n      : theme.fg(\"dim\", \"○ \") + item.text\n  );\n  return {\n    render: () => lines,\n    invalidate: () => {},\n  };\n});\n\n// Clear\nctx.ui.setWidget(\"my-widget\", undefined);\n```\n\n**예:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts)\n\n### 패턴 6: 사용자 정의 바닥글\n\n바닥글을 교체하세요. `footerData`는 확장 프로그램에서 액세스할 수 없는 데이터를 노출합니다.\n\n```typescript\nctx.ui.setFooter((tui, theme, footerData) => ({\n  invalidate() {},\n  render(width: number): string[] {\n    // footerData.getGitBranch(): string | null\n    // footerData.getExtensionStatuses(): ReadonlyMap<string, string>\n    return [`${ctx.model?.id} (${footerData.getGitBranch() || \"no git\"})`];\n  },\n  dispose: footerData.onBranchChange(() => tui.requestRender()), // reactive\n}));\n\nctx.ui.setFooter(undefined); // restore default\n```\n\n토큰 통계는 `ctx.sessionManager.getBranch()` 및 `ctx.model`를 통해 확인할 수 있습니다.\n\n**예:** [custom-footer.ts](../examples/extensions/custom-footer.ts)\n\n### 패턴 7: 사용자 정의 편집기(vim 모드 등)\n\n기본 입력 편집기를 사용자 정의 구현으로 바꿉니다. 모달 편집(vim), 다양한 키 바인딩(emacs) 또는 특수 입력 처리에 유용합니다.\n\n```typescript\nimport { CustomEditor, type ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { matchesKey, truncateToWidth } from \"@earendil-works/pi-tui\";\n\ntype Mode = \"normal\" | \"insert\";\n\nclass VimEditor extends CustomEditor {\n  private mode: Mode = \"insert\";\n\n  handleInput(data: string): void {\n    // Escape: switch to normal mode, or pass through for app handling\n    if (matchesKey(data, \"escape\")) {\n      if (this.mode === \"insert\") {\n        this.mode = \"normal\";\n        return;\n      }\n      // In normal mode, escape aborts agent (handled by CustomEditor)\n      super.handleInput(data);\n      return;\n    }\n\n    // Insert mode: pass everything to CustomEditor\n    if (this.mode === \"insert\") {\n      super.handleInput(data);\n      return;\n    }\n\n    // Normal mode: vim-style navigation\n    switch (data) {\n      case \"i\": this.mode = \"insert\"; return;\n      case \"h\": super.handleInput(\"\\x1b[D\"); return; // Left\n      case \"j\": super.handleInput(\"\\x1b[B\"); return; // Down\n      case \"k\": super.handleInput(\"\\x1b[A\"); return; // Up\n      case \"l\": super.handleInput(\"\\x1b[C\"); return; // Right\n    }\n    // Pass unhandled keys to super (ctrl+c, etc.), but filter printable chars\n    if (data.length === 1 && data.charCodeAt(0) >= 32) return;\n    super.handleInput(data);\n  }\n\n  render(width: number): string[] {\n    const lines = super.render(width);\n    // Add mode indicator to bottom border (use truncateToWidth for ANSI-safe truncation)\n    if (lines.length > 0) {\n      const label = this.mode === \"normal\" ? \" NORMAL \" : \" INSERT \";\n      const lastLine = lines[lines.length - 1]!;\n      // Pass \"\" as ellipsis to avoid adding \"...\" when truncating\n      lines[lines.length - 1] = truncateToWidth(lastLine, width - label.length, \"\") + label;\n    }\n    return lines;\n  }\n}\n\nexport default function (pi: ExtensionAPI) {\n  pi.on(\"session_start\", (_event, ctx) => {\n    // Factory receives the TUI, theme, and keybindings from the app\n    ctx.ui.setEditorComponent((tui, theme, keybindings) =>\n      new VimEditor(tui, theme, keybindings)\n    );\n  });\n}\n```\n\n**핵심 사항:**\n\n- **`CustomEditor`**(기본 `Editor` 아님)을 확장하여 앱 키 바인딩(중단하려면 이스케이프, 종료하려면 Ctrl+D, 모델 전환 등)을 가져옵니다.\n- **취급할 수 없는 키에 대해서는 `super.handleInput(data)`**로 전화하세요.\n- **팩토리 패턴**: `setEditorComponent`은 `tui`, `theme` 및 `keybindings`을 가져오는 팩토리 함수를 받습니다.\n- **기본 편집기를 복원하려면 `undefined`**를 전달하세요. `ctx.ui.setEditorComponent(undefined)`\n\n**예:** [modal-editor.ts](../examples/extensions/modal-editor.ts)\n\n## 주요 규칙\n\n1. **항상 콜백에서 테마 사용** - 테마를 직접 가져오지 마세요. `ctx.ui.custom((tui, theme, keybindings, done) =>...)` 콜백에서 `theme`를 사용하세요.\n\n2. **항상 DynamicBorder 색상 매개변수를 입력하세요** - `(s) => theme.fg(\"accent\", s)`가 아닌 `(s: string) => theme.fg(\"accent\", s)`를 쓰세요.\n\n3. **상태 변경 후 tui.requestRender() 호출** - `handleInput`에서 상태 업데이트 후 `tui.requestRender()`를 호출합니다.\n\n4. **3가지 메소드 객체 반환** - 맞춤 구성요소에는 `{ render, invalidate, handleInput }`가 필요합니다.\n\n5. **기존 구성요소 사용** - `SelectList`, `SettingsList`, `BorderedLoader` 사례의 90%를 다룹니다. 다시 빌드하지 마세요.\n\n## 예\n\n- **선택 UI**: [examples/extensions/preset.ts](../examples/extensions/preset.ts) - 동적 테두리 프레임이 포함된 SelectList\n- **취소를 통한 비동기**: [examples/extensions/qna.ts](../examples/extensions/qna.ts) - LLM 호출을 위한 BorderedLoader\n- **설정 토글**: [examples/extensions/tools.ts](../examples/extensions/tools.ts) - 도구 활성화/비활성화를 위한 설정 목록\n- **상태 표시기**: [examples/extensions/plan-mode/index.ts](../examples/extensions/plan-mode/index.ts) - setStatus 및 setWidget\n- **작업 표시기**: [examples/extensions/working-indicator.ts](../examples/extensions/working-indicator.ts) - setWorkingIndicator\n- **맞춤 바닥글**: [examples/extensions/custom-footer.ts](../examples/extensions/custom-footer.ts) - 통계가 포함된 setFooter\n- **사용자 정의 편집기**: [examples/extensions/modal-editor.ts](../examples/extensions/modal-editor.ts) - Vim과 유사한 모달 편집\n- **스네이크 게임**: [examples/extensions/snake.ts](../examples/extensions/snake.ts) - 키보드 입력, 게임 루프가 포함된 전체 게임\n- **사용자 정의 도구 렌더링**: [examples/extensions/todo.ts](../examples/extensions/todo.ts) - renderCall 및 renderResult","sourceFile":"tui.md"},"usage":{"title":"Pi 사용","markdown":"이 페이지는 빠른 시작 페이지에 맞지 않는 일상적인 사용 세부 정보를 수집합니다.\n\n## 대화형 모드\n\n<p align=\"center\"><img src=\"images/interactive-mode.png\" alt=\"Interactive Mode\" width=\"600\"></p>\n\n인터페이스에는 네 가지 주요 영역이 있습니다.\n\n- **시작 헤더** - 바로가기, 로드된 context files, prompt templates, 기술 및 확장 프로그램\n- **메시지** - 사용자 메시지, 어시스턴트 응답, 도구 호출, 도구 결과, 알림, 오류 및 확장 UI\n- **편집기** - 입력하는 곳입니다. 테두리 색상은 현재 사고 수준을 나타냅니다.\n- **바닥글** - 작업 디렉터리, 세션 이름, 토큰/캐시 사용량, 비용, 컨텍스트 사용량 및 현재 모델입니다. 총계에는 보조자 응답, 도구에서 보고된 사용량 및 요약 생성이 포함됩니다.\n\n편집기는 `/settings`와 같은 내장 UI나 사용자 정의 확장 UI로 일시적으로 대체될 수 있습니다.\n\n### 편집기 기능\n\n| 특징 | 어떻게 |\n|---------|-----|\n| 파일 참조 | 프로젝트 파일을 퍼지 검색하려면 `@`를 입력하세요. |\n| 경로 완성 | 경로를 완성하려면 Tab 키를 누르세요. |\n| 다중 라인 입력 | Shift+Enter 또는 Windows 터미널의 경우 Ctrl+Enter |\n| 응답 복사 | Ctrl+X는 마지막 보조 메시지를 복사합니다. `/tree`에서는 선택한 메시지를 복사합니다. |\n| 이미지 | Windows에서는 Ctrl+V, Alt+V를 사용하여 붙여넣거나 터미널로 드래그하세요. |\n| 쉘 명령 | `!command` 실행하고 모델에 출력을 보냅니다. |\n| 숨겨진 쉘 명령 | `!!command` 모델에 출력을 보내지 않고 실행됩니다. |\n| 외부 편집자 | Ctrl+G는 `externalEditor`, `$VISUAL`, `$EDITOR`, Windows에서는 메모장, 다른 곳에서는 `nano`를 엽니다. |\n\n모든 단축키 및 사용자 정의는 [Keybindings](keybindings.md)를 참조하세요.\n\n## 슬래시 명령\n\n편집기에서 `/`를 입력하여 명령 완성을 엽니다. Extensions는 커스텀 명령어를 등록할 수 있고, 스킬은 `/skill:name`로 사용 가능하며, prompt templates는 `/templatename`를 통해 확장 가능합니다.\n\n| 명령 | 설명 |\n|---------|-------------|\n| `/login`, `/logout` | OAuth 또는 API 키 자격 증명 관리 |\n| [`/llama`](llama-cpp.md) | llama.cpp 라우터 모델 다운로드, 로드 및 언로드 |\n| `/model` | 모델 전환 |\n| `/scoped-models` | Ctrl+P 사이클링을 위한 모델 활성화/비활성화 |\n| `/settings` | 사고수준, 테마, 메시지 전달, 전송 |\n| `/resume` | Pi이전 세션에서 확인 |\n| `/new` | 새 세션 시작 |\n| `/name <name>` | 세션 표시 이름 설정 |\n| `/session` | 세션 파일, ID, 메시지, 토큰 및 비용 표시 |\n| `/tree` | 세션의 어느 지점으로든 이동하고 거기서부터 계속하세요. |\n| `/trust` | 향후 세션을 위해 프로젝트 신뢰 결정 저장 |\n| `/fork` | 이전 사용자 메시지에서 새 세션 만들기 |\n| `/clone` | 현재 활성 분기를 새 세션에 복제합니다. |\n| `/compact [prompt]` | 수동으로 압축된 컨텍스트(선택적으로 사용자 지정 지침 포함) |\n| `/copy` | 마지막 어시스턴트 메시지를 클립보드에 복사 |\n| `/export [file]` | 세션을 HTML 또는 JSONL로 내보내기 |\n| `/import <file>` | JSONL 파일에서 세션 가져오기 및 재개 |\n| `/share` | 공유 가능한 HTML 링크가 포함된 비공개 GitHub 요점으로 업로드 |\n| `/reload` | 키 바인딩, 확장 프로그램, 기술, 프롬프트, 테마 및 context files를 다시 로드하세요. |\n| `/hotkeys` | 모든 키보드 단축키 표시 |\n| `/changelog` | 버전 기록 표시 |\n| `/quit` | 파이 그만둬 |\n\n## 메시지 대기열\n\n에이전트가 계속 작업하는 동안 메시지를 제출할 수 있습니다.\n\n- **Enter**는 현재 보조 턴이 도구 호출 실행을 마친 후에 전달되는 조정 메시지를 대기열에 넣습니다.\n- **Alt+Enter**는 상담사가 모든 작업을 마친 후 전달되는 후속 메시지를 대기열에 추가합니다.\n- **Escape**는 대기 중인 메시지를 중단하고 편집기에 복원합니다.\n- **Alt+Up**은 대기 중인 메시지를 편집기로 다시 검색합니다.\n\nWindows 터미널에서는 Alt+Enter가 기본적으로 전체 화면입니다. pi가 바로가기를 수신하도록 하려면 [Terminal setup](terminal-setup.md)에 설명된 대로 다시 매핑하세요.\n\n`steeringMode` 및 `followUpMode`를 사용하여 [Settings](settings.md)에서 게재를 구성합니다.\n\n## 세션\n\n세션은 작업 디렉토리별로 구성되어 `~/.pi/agent/sessions/`에 자동으로 저장됩니다.\n\n```bash\npi -c                  # Continue most recent session\npi -r                  # Browse and select a session\npi --no-session        # Ephemeral mode; do not save\npi --name \"my task\"    # Set session display name at startup\npi --session <path|id> # Use a specific session file or session ID\npi --fork <path|id>    # Fork a session into a new session file\n```\n\n유용한 세션 명령:\n\n- `/session` 현재 세션 파일과 ID를 보여줍니다.\n- `/tree`는 파일 내 session tree를 탐색하고 버려진 분기를 요약할 수 있습니다.\n- `/fork` 이전 사용자 메시지에서 새 세션을 생성합니다.\n- `/clone` 현재 활성 분기를 새 세션 파일에 복제합니다.\n- `/compact` 오래된 메시지를 자유로운 맥락으로 요약합니다.\n\n자세한 내용은 [Sessions](sessions.md) 및 [Compaction](compaction.md)를 참조하세요.\n\n## 컨텍스트 파일\n\nPi는 시작 시 다음 위치에서 `AGENTS.md` 또는 `CLAUDE.md`를 로드합니다.\n\n- `~/.pi/agent/AGENTS.md` 글로벌 지침\n- 상위 디렉토리, 현재 작업 디렉토리에서 위로 이동\n- 현재 디렉토리\n\n디렉토리에 `AGENTS.override.md`가 포함되어 있으면 Pi는 해당 디렉토리에서 `AGENTS.md` 또는 `CLAUDE.md` 대신 해당 디렉토리를 로드합니다. 다른 디렉터리의 컨텍스트 파일은 여전히 ​​정상적으로 계층화됩니다.\n\n프로젝트 규칙, 명령, 안전 규칙 및 기본 설정에는 context files를 사용하세요. `--no-context-files` 또는 `-nc`로 로딩을 비활성화합니다.\n\n### 시스템 프롬프트 파일\n\n기본 시스템 프롬프트를 다음으로 바꾸십시오.\n\n- `.pi/SYSTEM.md` 프로젝트의 경우\n- `~/.pi/agent/SYSTEM.md` 전 세계적으로\n\n두 위치 모두에서 `APPEND_SYSTEM.md`로 바꾸지 않고 기본 프롬프트에 추가합니다.\n\n### 프로젝트 신뢰\n\n대화형 시작 시 pi는 프로젝트 로컬 설정, 리소스 또는 프로젝트 `.agents/skills`를 포함하고 `~/.pi/agent/trust.json`의 폴더 또는 상위 폴더에 대해 저장된 결정이 없는 프로젝트 폴더를 신뢰하기 전에 묻습니다. 프로젝트를 신뢰하면 pi가 `.pi/settings.json` 및 `.pi` 리소스를 로드하고, 누락된 프로젝트 패키지를 설치하고, 프로젝트 확장을 실행할 수 있습니다.\n\n신뢰 결정 전에 pi는 context files, 사용자/전역 확장 및 CLI `-e` 확장만 로드하여 `project_trust` 이벤트를 처리할 수 있습니다. 프로젝트 로컬 확장, 프로젝트 패키지 관리 확장 및 프로젝트 설정은 프로젝트를 신뢰한 후에만 로드됩니다. 이 분할은 현재 프로세스에서 신뢰가 확인되지 않은 다른 cwd의 세션으로 전환할 때도 적용됩니다.\n\n비대화형 모드(`-p`, `--mode json` 및 `--mode rpc`)에는 신뢰 프롬프트가 표시되지 않습니다. 적용 가능한 저장된 신뢰 결정이 없으면 전역 설정에서 `defaultProjectTrust`를 사용합니다. `ask`(기본값) 및 `never`는 해당 프로젝트 리소스를 무시하고 `always`는 이를 신뢰합니다. 한 번의 실행에 대해 프로젝트 신뢰를 재정의하려면 `--approve`/`-a` 또는 `--no-approve`/`-na`를 전달합니다.\n\n확장이나 저장된 결정이 적용되지 않는 경우 `defaultProjectTrust`는 대체 동작을 제어합니다. `~/.pi/agent/settings.json`에서는 `\"ask\"`, `\"always\"`, `\"never\"`로 설정하거나 `/settings`로 변경하세요.\n\n`pi config` 및 패키지 명령은 동일한 프로젝트 신뢰 흐름을 사용합니다. 단, `pi update`는 메시지를 표시하지 않습니다. 하나의 명령에 대해 프로젝트 로컬 설정을 신뢰하려면 `--approve`를 전달하고 이를 무시하려면 `--no-approve`를 전달합니다.\n\n직계 상위 폴더에 대한 신뢰를 포함하여 향후 세션에 대한 프로젝트 신뢰 결정을 저장하려면 대화형 모드에서 `/trust`를 사용하세요. `~/.pi/agent/trust.json`만 씁니다. 현재 세션은 다시 로드되지 않으므로 변경 사항을 적용하려면 pi를 다시 시작하세요.\n\n\n## 세션 내보내기 및 공유\n\nHTML에 세션을 작성하려면 `/export [file]`를 사용하세요.\n\n공유 가능한 HTML 링크가 포함된 비공개 GitHub 요점을 업로드하려면 `/share`를 사용하세요.\n\n오픈 소스 작업에 pi를 사용하고 모델, 프롬프트, 도구 및 평가 연구를 위한 세션을 게시하려면 [`badlogic/pi-share-hf`](https://github.com/badlogic/pi-share-hf)를 참조하세요. Hugging Face 데이터 세트에 세션을 게시합니다.\n\n## CLI 참고\n\n```bash\npi [options] [@files...] [messages...]\n```\n\n### 패키지 명령\n\n```bash\npi install <source> [-l]     # Install package, -l for project-local\npi remove <source> [-l]      # Remove package\npi uninstall <source> [-l]   # Alias for remove\npi update [source|self|pi]   # Update pi only, or one package source\npi update --all              # Update pi and packages; reconcile pinned git refs\npi update --extensions       # Update packages only; reconcile pinned git refs\npi update --models           # Refresh model catalogs only\npi update --self             # Update pi only\npi update --extension <src>  # Update one package\npi list                      # List installed packages\npi config                    # Enable/disable package resources\n```\n\n이 명령은 pi 패키지를 관리하고 `pi update`는 pi CLI 설치를 업데이트할 수 있습니다. pi 자체를 제거하려면 [Quickstart](quickstart.md#uninstall)를 참조하세요. `pi config` 및 프로젝트 패키지 명령은 `--approve`/`--no-approve`를 허용하여 하나의 명령에 대한 프로젝트 로컬 설정을 신뢰하거나 무시합니다. `pi update` 프로젝트 신뢰를 묻는 메시지가 표시되지 않습니다.\n\n패키지 소스 및 보안 참고사항은 [Pi Packages](packages.md)를 참조하세요.\n\n### 모드\n\n| 깃발 | 설명 |\n|------|-------------|\n| 기본 | 대화형 모드 |\n| `-p`, `--print` | 응답 인쇄 및 종료 |\n| `--mode json` | 모든 이벤트를 JSON 라인으로 출력합니다. [JSON mode](json.md) 참조 |\n| `--mode rpc` | RPC 모드 이상 stdin/stdout; [RPC mode](rpc.md) 참조 |\n| `--export <in> [out]` | 세션을 HTML로 내보내기 |\n\n인쇄 모드에서 pi는 파이프로 연결된 stdin도 읽고 이를 초기 프롬프트에 병합합니다.\n\n```bash\ncat README.md | pi -p \"Summarize this text\"\n```\n\n### 모델 옵션\n\n| 옵션 | 설명 |\n|--------|-------------|\n| `--provider <name>` | `anthropic`, `openai`, `google` 등의 제공자 |\n| `--model <pattern>` | 모델 패턴 또는 ID `provider/id` 지원 및 선택 사항 `:<thinking>` |\n| `--api-key <key>` | API key, 환경 변수 재정의 |\n| `--thinking <level>` | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max` |\n| `--models <patterns>` | Ctrl+P 순환을 위한 쉼표로 구분된 패턴 |\n| `--list-models [search]` | 사용 가능한 모델 나열 |\n\n### 세션 옵션\n\n| 옵션 | 설명 |\n|--------|-------------|\n| `-c`, `--continue` | 가장 최근 세션 계속하기 |\n| `-r`, `--resume` | 세션 찾아보기 및 선택 |\n| `--세션 <경로\\ | 아이디>` | 특정 세션 파일 또는 부분 UUID 사용 |\n| `--fork <경로\\ | 아이디>` | 세션 파일 또는 부분 UUID를 새 세션으로 포크 |\n| `--session-dir <dir>` | 사용자 정의 세션 저장소 디렉터리 |\n| `--no-session` | 임시 모드; 저장하지 마세요 |\n| `--name <name>`, `-n <name>` | 시작 시 세션 표시 이름 설정 |\n\n### 도구 옵션\n\n| 옵션 | 설명 |\n|--------|-------------|\n| `--tools <list>`, `-t <list>` | 특정 내장, 확장 및 맞춤 도구를 허용 목록에 추가 |\n| `--exclude-tools <list>`, `-xt <list>` | 특정 내장, 확장 및 사용자 정의 도구 비활성화 |\n| `--no-builtin-tools`, `-nbt` | 내장 도구를 비활성화하지만 확장/사용자 정의 도구는 활성화된 상태로 유지합니다. |\n| `--no-tools`, `-nt` | 모든 도구 비활성화 |\n\n내장 도구: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`.\n\n### 자원 옵션\n\n| 옵션 | 설명 |\n|--------|-------------|\n| `-e`, `--extension <source>` | 경로, npm 또는 git에서 확장 프로그램을 로드합니다. 반복 가능 |\n| `--no-extensions` | 확장 검색 비활성화 |\n| `--skill <path>` | 스킬을 로드하세요. 반복 가능 |\n| `--no-skills` | 기술 발견 비활성화 |\n| `--prompt-template <path>` | 프롬프트 템플릿을 로드합니다. 반복 가능 |\n| `--no-prompt-templates` | 프롬프트 템플릿 검색 비활성화 |\n| `--theme <path>` | 테마를 로드하세요. 반복 가능 |\n| `--no-themes` | 테마 검색 비활성화 |\n| `--no-context-files`, `-nc` | `AGENTS.md` 및 `CLAUDE.md` 검색 비활성화 |\n\n`--no-*`를 명시적 플래그와 결합하면 설정을 무시하고 필요한 것을 정확하게 로드할 수 있습니다. 예:\n\n```bash\npi --no-extensions -e ./my-extension.ts\n```\n\n### 기타 옵션\n\n| 옵션 | 설명 |\n|--------|-------------|\n| `--system-prompt <text>` | 기본 프롬프트를 교체합니다. context files 및 스킬은 여전히 ​​추가됩니다. |\n| `--append-system-prompt <text>` | 시스템 프롬프트에 추가 |\n| `--tui-mode <mode>` | TUI 모드: `regular`(기본값) 또는 실험적 `fullscreen` |\n| `--verbose` | 강제로 자세한 시작 시작 |\n| `-a`, `--approve` | 이 실행을 위해 프로젝트 로컬 파일을 신뢰합니다. |\n| `-na`, `--no-approve` | 이 실행에 대해 프로젝트 로컬 파일을 무시합니다. |\n| `-h`, `--help` | 도움말 표시 |\n| `-v`, `--version` | 버전 표시 |\n\n`fullscreen` 모드에서는 대기 중인 메시지, 작업 상태, 확장 위젯, 편집기 및 바닥글이 하단에 고정되어 있는 동안 스크립트가 터미널 뷰포트 내부에서 스크롤됩니다. 마우스/트랙패드 입력은 포인터 아래 영역을 스크롤합니다. 키보드 뷰포트 작업은 항상 사용할 수 있습니다. 인라인 이미지는 Kitty 및 Ghostty를 포함하여 Kitty 그래픽 프로토콜을 지원하는 터미널에서 작동합니다. iTerm2에서는 인라인 이미지 프로토콜이 애플리케이션 소유 스크롤 중에 배치를 삭제하거나 자를 수 없기 때문에 텍스트 자리 표시자로 렌더링됩니다. `regular` 모드에서 pi는 기본 화면과 터미널 소유 스크롤백을 사용하고 iTerm2 인라인 이미지는 계속해서 정상적으로 렌더링됩니다.\n\n`/settings`에서 **TUI 모드**를 설정하면 `regular`과 `fullscreen` 사이를 즉시 전환하고 향후 세션에 대한 기본값을 선택할 수 있습니다. **전체 화면 종료 출력**은 전체 화면 종료 시 최종 스크립트를 인쇄할지 아니면 이전 화면을 복원하고 세션 재개 힌트만 인쇄할지 여부를 제어합니다.\n\n### 파일 인수\n\n메시지에 파일을 포함하려면 파일 앞에 `@` 접두사를 붙이세요.\n\n```bash\npi @prompt.md \"Answer this\"\npi -p @screenshot.png \"What's in this image?\"\npi @code.ts @test.ts \"Review these files\"\n```\n\n### 예\n\n```bash\n# Interactive with initial prompt\npi \"List all .ts files in src/\"\n\n# Non-interactive\npi -p \"Summarize this codebase\"\n\n# Non-interactive with piped stdin\ncat README.md | pi -p \"Summarize this text\"\n\n# Named one-shot session\npi --name \"release audit\" -p \"Audit this repository\"\n\n# Different model\npi --provider openai --model gpt-4o \"Help me refactor\"\n\n# Model with provider prefix\npi --model openai/gpt-4o \"Help me refactor\"\n\n# Model with thinking level shorthand\npi --model sonnet:high \"Solve this complex problem\"\n\n# Limit model cycling\npi --models \"claude-*,gpt-4o\"\n\n# Read-only mode\npi --tools read,grep,find,ls -p \"Review the code\"\n\n# Disable one extension or built-in tool while keeping the rest available\npi --exclude-tools ask_question\n```\n\n## 디자인 원칙\n\nPi는 핵심을 작게 유지하고 워크플로별 동작을 확장, 기술, prompt templates 및 패키지로 푸시합니다.\n\n의도적으로 내장 MCP, 하위 에이전트, 권한 팝업, 계획 모드, 할 일 또는 배경 bash을 포함하지 않습니다. 이러한 워크플로를 확장 프로그램이나 패키지로 구축 또는 설치할 수도 있고, 컨테이너 및 tmux와 같은 외부 도구를 사용할 수도 있습니다.\n\n전체 근거를 보려면 [blog post](https://mariozechner.at/posts/2025-11-30-pi-coding-agent/)를 읽어보세요.","sourceFile":"usage.md"},"windows":{"title":"윈도우 설치","markdown":"Pi Windows에서는 bash 셸이 필요합니다. 확인된 위치(순서):\n\n1. `~/.pi/agent/settings.json`의 맞춤 경로\n2. Git 배쉬 (`C:\\Program Files\\Git\\bin\\bash.exe`)\n3. PATH(Cygwin, MSYS2, WSL)의 `bash.exe`\n\n대부분의 사용자에게는 [Git for Windows](https://git-scm.com/download/win)이면 충분합니다.\n\n## 사용자 정의 셸 경로\n\n```json\n{\n  \"shellPath\": \"C:\\\\cygwin64\\\\bin\\\\bash.exe\"\n}\n```","sourceFile":"windows.md"}}},"navigation":{"ko":[{"title":"여기서 시작","items":[{"title":"Pi 문서","path":"/docs/latest","slug":"index"},{"title":"빠른 시작","path":"/docs/latest/quickstart","slug":"quickstart"},{"title":"Pi 사용","path":"/docs/latest/usage","slug":"usage"},{"title":"Providers","path":"/docs/latest/providers","slug":"providers"},{"title":"보안","path":"/docs/latest/security","slug":"security"},{"title":"컨테이너화","path":"/docs/latest/containerization","slug":"containerization"},{"title":"설정","path":"/docs/latest/settings","slug":"settings"},{"title":"키바인딩","path":"/docs/latest/keybindings","slug":"keybindings"},{"title":"세션","path":"/docs/latest/sessions","slug":"sessions"},{"title":"압축 및 분기 요약","path":"/docs/latest/compaction","slug":"compaction"}]},{"title":"사용자 지정","items":[{"title":"Extensions","path":"/docs/latest/extensions","slug":"extensions"},{"title":"Skills","path":"/docs/latest/skills","slug":"skills"},{"title":"프롬프트 템플릿","path":"/docs/latest/prompt-templates","slug":"prompt-templates"},{"title":"테마","path":"/docs/latest/themes","slug":"themes"},{"title":"Pi Packages","path":"/docs/latest/packages","slug":"packages"},{"title":"맞춤 Models","path":"/docs/latest/models","slug":"models"},{"title":"맞춤 Providers","path":"/docs/latest/custom-provider","slug":"custom-provider"}]},{"title":"참조","items":[{"title":"세션 파일 형식","path":"/docs/latest/session-format","slug":"session-format"}]},{"title":"프로그래밍 방식 사용","items":[{"title":"SDK","path":"/docs/latest/sdk","slug":"sdk"},{"title":"RPC 모드","path":"/docs/latest/rpc","slug":"rpc"},{"title":"JSON 이벤트 스트림 모드","path":"/docs/latest/json","slug":"json"},{"title":"TUI 구성품","path":"/docs/latest/tui","slug":"tui"}]},{"title":"플랫폼 설정","items":[{"title":"윈도우 설치","path":"/docs/latest/windows","slug":"windows"},{"title":"Termux (안드로이드) 설정","path":"/docs/latest/termux","slug":"termux"},{"title":"tmux 설정","path":"/docs/latest/tmux","slug":"tmux"},{"title":"터미널 설정","path":"/docs/latest/terminal-setup","slug":"terminal-setup"},{"title":"쉘 별칭","path":"/docs/latest/shell-aliases","slug":"shell-aliases"}]},{"title":"개발","items":[{"title":"개발","path":"/docs/latest/development","slug":"development"}]}]}}
