SLOPSHOPPER

mod-store

k-mods 카탈로그를 한 패널에서 보고 설치·제거하는 모드 상점. /k-mods 하나면 충분해요.

newpanecommandtoastprocess
★ 1v1.0.0MITupdated 2026-10-08SeongGwangJu/k-mods/mods/mod-store
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · mod-store
│ ┃ k-mods 모드 상점 ✕ › fix the failing auth test an╭────────────────────────────────────╮ │ ┃ k-mods 모드 상점 │ mod-store │ │ ┃ ⏺ Read(src/auth.ts) │ k-mods 설치 완료 · /k-mods 로 모드를 둘러보세요 │ │ ┃ 0: 전체 (66) 1: 한국어화 (1) 2: 테마 (3) ⎿ Read 6 lines ╰────────────────────────────────────╯ │ ┃ ⏺ Update(src/auth.ts) │ ┃ 원본 한국어 UI 번역 사전 ⎿ Added 2 lines, removed 1 line │ ┃ 대화 읽기 ⏺ Bash(bun test) │ ┃ [ 설치 ] 자세히 ⎿ 3 pass, 1 fail │ ┃ │ ┃ 수정판 스킨 (한국 수정판) ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ 도구 호출 제어 │ ┃ [ 설치 ] 자세히 ✻ Worked for 42s · done 4:20 PM │ ┃ │ ┃ 수정판 픽셀 장면 스피너 (한국 수정판) › /k-mods │ ┃ 프로그램 실행 │ ┃ [ 설치 ] 자세히 │ ┃ │ ┃ 오리지널 작업 상태 한국어 │ ┃ 도구 호출 제어 │ ┃ [ 설치 ] 자세히 │ ┃ │ ┃ 오리지널 스트리머 모드 │ ┃ 대화 읽기 │ ┃ [ 설치 ] 자세히 │ ┃ │ ┃ 묶음 한국어 팩 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · k-mods 모드 상점
k-mods 모드 상점 0: 전체 (66) 1: 한국어화 (1) 2: 테마 (3) 3: 애니메이션 (8 원본 한국어 UI 번역 사전 대화 읽기 [ 설치 ] 자세히 수정판 스킨 (한국 수정판) 도구 호출 제어 [ 설치 ] 자세히 수정판 픽셀 장면 스피너 (한국 수정판) 프로그램 실행 [ 설치 ] 자세히 오리지널 작업 상태 한국어 도구 호출 제어 [ 설치 ] 자세히 오리지널 스트리머 모드 대화 읽기 [ 설치 ] 자세히 묶음 한국어 팩 도구 호출 제어 [ 설치 ] 자세히 원본 GFM 렌더러 프로그램 실행 [ 설치 ] 자세히 원본 프리즈만티스 대화 읽기 [ 설치 ] 자세히 원본 CC 아케이드 도구 호출 제어 [ 설치 ] 자세히 원본 Clawd 스피너 대화 읽기 [ 설치 ] 자세히 원본 Clawd 이야기 도구 호출 제어 [ 설치 ] 자세히 원본 콤보 미터 도구 호출 제어 [ 설치 ] 자세히 원본 디프 인베이더 도구 호출 제어 [ 설치 ] 자세히 원본 릴스 네트워크 [ 설치 ] 자세히 원본 툴 디펜스 도구 호출 제어 [ 설치 ] 자세히 오리지널 컨텍스트 막대 프로그램 실행 [ 설치 ] 자세히 원본 에이전트 레이더 도구 호출 제어 [ 설치 ] 자세히 원본 브라우저 레인 프로그램 실행 [ 설치 ] 자세히 원본 캐시 패널 프로그램 실행 [ 설치 ] 자세히 원본 컨텍스트 바 (hamzafer) 대화 읽기 [ 설치 ] 자세히 원본 플라이트덱 대시보드 도구 호출 제어 [ 설치 ] 자세히 원본 claude-hud 상태 대시보드 프로그램 실행 [ 설치 ] 자세히 원본 사용량 진행률 막대 프로그램 실행 [ 설치 ] 자세히 원본 PR 펄스 프로그램 실행 [ 설치 ] 자세히 원본 프롬프트 캐시 미터 환경·설정 읽기 [ 설치 ] 자세히 원본 턴 영수증 도구 호출 제어 [ 설치 ] 자세히 원본 리뷰 워치 프로그램 실행 [ 설치 ] 자세히 원본 상태 카드 프로그램 실행 [ 설치 ] 자세히 원본 택시 미터기 대화 읽기 [ 설치 ] 자세히 원본 할 일 진행 상태줄 도구 호출 제어 [ 설치 ] 자세히 원본 토큰 날씨 대화 읽기 [ 설치 ] 자세히 원본 테일스케일 노드 상태줄 프로그램 실행 [ 설치 ] 자세히 원본 사용량 밴드 (5시간·주간) 프로그램 실행 [ 설치 ] 자세히 오리지널 메모장 프롬프트 입력 [ 설치 ] 자세히 원본 에이전트 플로우 도구 호출 제어 [ 설치 ] 자세히 원본 퀵 메뉴 환경·설정 읽기 [ 설치 ] 자세히 원본 이미지 미리보기 프로그램 실행 [ 설치 ] 자세히 원본 나우 플레잉 네트워크 [ 설치 ] 자세히 원본 붙여넣기 미리보기 프로그램 실행 [ 설치 ] 자세히 원본 픽셀 뮤직 플레이어 프로그램 실행 [ 설치 ] 자세히 원본 프롬프트 레일 프로그램 실행 [ 설치 ] 자세히 원본 Replay 극장 도구 호출 제어 [ 설치 ] 자세히 원본 세션 랩드 프로그램 실행 [ 설치 ] 자세히 원본 택시 블랙박스 도구 호출 제어 [ 설치 ] 자세히 원본 택시 내비게이션 도구 호출 제어 [ 설치 ] 자세히 오리지널 작업 끝 알림 네트워크 [ 설치 ] 자세히 원본 컴팩트 타이밍 어드바이저 네트워크 [ 설치 ] 자세히 원본 오토 핸드오프 프로그램 실행 [ 설치 ] 자세히 원본 컨텍스트 핸드오프 파일 쓰기 [ 설치 ] 자세히 원본 스킬 자동 추천 네트워크 [ 설치 ] 자세히 원본 다음 할 일 제안 모델 호출 [ 설치 ] 자세히 원본 스위치보드 네트워크 [ 설치 ] 자세히 원본 현재 상황 요약 모델 호출 [ 설치 ] 자세히 수정판 위험 명령 브레이크 프로그램 실행 [ 설치 ] 자세히 원본 시크릿 리댁터 도구 호출 제어 [ 설치 ] 자세히 원본 시크릿 볼트 도구 호출 제어 [ 설치 ] 자세히 원본 택시 과속카메라 도구 호출 제어 [ 설치 ] 자세히 원본 GitHub PR 트래커 프로그램 실행 [ 설치 ] 자세히 원본 GitHub 이슈 패널 프로그램 실행 [ 설치 ] 자세히 원본 글랜스 네트워크 [ 설치 ] 자세히 원본 Linear 보드 패널 네트워크 [ 설치 ] 자세히 원본 Linear 티켓 패널 네트워크 [ 설치 ] 자세히 원본 터미널 브라우저 네트워크 [ 설치 ] 자세히 원본 Vercel 배포 현황 프로그램 실행 [ 설치 ] 자세히 묶음 추천 세트 네트워크 [ 설치 ] 자세히 묶음 택시팩 도구 호출 제어 [ 설치 ] 자세히 Tab 이동 · Enter 누르기 · Esc 닫기
README

k-mods 모드 상점

k-mods 카탈로그를 패널 하나로 둘러보고, 키 하나로 설치·제거해요. /plugin install <이름>@k-mods를 외울 필요가 없어요.

╭────────────────────────────────────────────────────────────────────────────────────────────────✕─╮
│ k-mods 모드 상점                                                                                 │
│                                                                                                  │
│ 0: 전체 (10)  1: 한국어화 (1)  2: 테마 (1)  3: 애니메이션 (1)  4: 상태줄 (2)  5: 도구 (1)        │
│ 6: 자동화 (1)  7: 지킴이 (2)  8: 연동 (0)  9: 묶음 (1)                                           │
│                                                                                                  │
│  오리지널  한글 상태표시줄                                                                       │
│ 프롬프트 아래 상태줄을 한국어로 보여줘요.                                                        │
│ 화면만  [ 설치 ]  자세히                                                                         │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯

설치

/plugin marketplace add SeongGwangJu/k-mods
/plugin install mod-store@k-mods

쓰는 법

/k-mods를 입력하면 패널이 열려요(이미 열려 있으면 닫혀요). 작업 중에도 열 수 있어요.

  • 숫자 탭(0~9)으로 카테고리를 거를 수 있어요. 탭에 초점이 있으면 숫자만 눌러도 바로 넘어가요.
  • 설치 버튼을 누르면 설치가 시작돼요. 이미 설치된 항목은 설치됨 · 제거로 바뀌고, 누르면 정말 지울지 한 번 물어봐요.
  • 자세히를 누르면 제작자·라이선스·명령·준비물·주의사항·홈페이지가 펼쳐져요.
  • 설치가 끝나면 토스트가 뜨고, 지금 적용 버튼으로 /reload-plugins를 바로 실행할 수 있어요.
  • 목록이 패널보다 길면 패널째로 스크롤돼요(위/아래 화살표, 페이지 업/다운).
  • Esc로 닫혀요.

설정 (/config 또는 /plugin configure mod-store@k-mods)

따로 설정할 값은 없어요.

어떻게 동작하나요

  • /k-mods 명령은 immediate: true로 등록해서, Claude가 작업 중에도 바로 열려요.
  • 패널은 $.ui.resolve로 받은 Box/Text/Button/Link만으로 그려요. 폭이 100칸 이상이면 넓음, 70~99칸이면 보통, 그 아래는 좁음으로 보고, 좁을수록 요약 문장을 줄이고 권한은 가장 민감한 것 하나만 보여줘요(실제 세션에서 확인: e.props.bodyColumns는 터미널 전체 폭보다 테두리만큼 좁아서, 70칸짜리 터미널은 "좁음" 단계로 그려져요).
  • 설치·제거는 실제로 어떻게 되나요: /plugin install <이름>@k-mods를 세션 안에서 흉내 내는 방법($.command.run({ command: 'plugin', args: 'install ...' }))은 설치를 바로 하지 않고 /plugin 패널의 Discover 탭을 여는 것으로 끝나요(공식 문서로 확인, 아래 "한계" 참고). 그래서 이 mod는 $.process.run(['claude', 'plugin', 'install', '<이름>@k-mods', '--scope', 'user'])로 셸의 claude 명령을 직접 실행해요. 누르기 전에 claude --version으로 2.1.287 이상인지 먼저 확인하고, 아니거나 명령이 없으면 복사할 수 있는 명령과 함께 한국어로 알려줘요. 설치 중 "마켓플레이스가 없다"는 응답을 받으면 /plugin marketplace add SeongGwangJu/k-mods를 복사 버튼과 함께 안내해요.
  • 설치 상태는 $.settings.read()의 enabledPlugins["<이름>@k-mods"]가 true인지로 판단하고, 패널을 열 때와 설치·제거 직후에 다시 읽어요.
  • 처음 켜질 때 한 번 k-mods 설치 완료 · /k-mods 로 모드를 둘러보세요 토스트를 보여줘요($.store에 welcomed 표시를 남겨서 다음부터는 안 떠요).

권한 (이 mod가 내 컴퓨터에서 하는 일)

  • 화면: 패널을 그려요.
  • 명령 실행: claude --version, claude plugin install ..., claude plugin uninstall ...만 실행해요. 그 외 어떤 프로그램도 실행하지 않아요.
  • 설정 읽기: $.settings.read()로 enabledPlugins만 읽어요(쓰지 않아요).
  • 저장소: welcomed 표시 하나만 $.store에 저장해요.
  • 네트워크: 직접 호출하지 않아요. 네트워크가 필요한 건 claude plugin install이 알아서 하는 몫이에요(마켓플레이스 메타데이터 내려받기).

한계

  • 아직 GitHub에 올라가지 않은 저장소라, claude plugin install 자체는 "마켓플레이스를 찾을 수 없다"로 끝나는 걸 실제 세션에서 확인했어요(검증 방법은 README 하단의 참고 사항 대신 PR 설명에 적어 둘게요). 마켓플레이스를 추가한 뒤에는 그대로 동작해요.
  • /plugin install <이름>@k-mods를 세션 안에서 대신 실행하는 방법은 쓰지 않아요. 패널만 열고 설치는 안 해서, 한 번에 설치된다는 기대를 깨기 때문이에요.
  • $.command.register로 등록한 /k-mods라는 이름을 다른 mod가 먼저 차지하면 조용히 넘어가요(패널을 열 방법이 없어진다는 뜻이라, 이 경우 claude plugin list로 충돌하는 mod를 확인해 주세요).
  • 카탈로그(hooks/catalog.ts)는 지금은 손으로 채운 값이고, 나중에 scripts/build.mjs가 registry/*.json으로부터 다시 만들어요.

출처·라이선스

오리지널. hooks/catalog.ts의 카탈로그 항목 설명은 각 mod의 registry/<name>.json을 요약한 것이고, 원본 출처·라이선스는 각 mod 자신의 README에 있어요.

테스트한 Claude Code 버전: 2.1.291

Source 4 files
hooks/register.ts 265 lines
1import type { EngineInterface, On } from 'claude-code'
2
3import { CATALOG, SELF_NAME } from './catalog'
4import {
5  type Banner,
6  MARKETPLACE_ADD_COMMAND,
7  MIN_CLAUDE_VERSION,
8  errorBannerOf,
9  firstVersionToken,
10  installArgsFor,
11  installedNamesFrom,
12  isVersionAtLeast,
13  messageOf,
14  parseInstallOutcome,
15  parseUninstallOutcome,
16  uninstallArgsFor,
17  visibleCatalog,
18  widthTierOf,
19} from './lib'
20import { paneView, type StoreActions, type StoreModel } from './view'
21
22const PANE_ID = 'k-mods-store'
23const PANE_TITLE = 'k-mods 모드 상점'
24const COMMAND_NAME = 'k-mods'
25
26// ---------------------------------------------------------------------------
27// 모듈 상태. 모듈이 새로 로드(핫 리로드)될 때까지만 유지된다 — CONTRIBUTING.md의
28// "모듈 전역 변수" 수명과 같다. $.state를 쓰지 않는 이유: 패널을 쓰는 동안만
29// 의미 있는 값들이라 세션을 넘어 지킬 필요가 없다.
30// ---------------------------------------------------------------------------
31
32let tab: StoreModel['tab'] = 'all'
33let expanded: string | null = null
34let installed = new Set<string>()
35let busy = new Set<string>()
36let banner: Banner = { kind: 'none' }
37let justInstalled: string | null = null
38
39const CATALOG_VISIBLE = visibleCatalog(CATALOG, SELF_NAME)
40
41// ---------------------------------------------------------------------------
42// $를 받는 도우미. 모두 이 파일(register.ts) 최상위에 선언했다 — mods API는
43// 같은 파일 최상위 함수에만 넘길 수 있고, 클로저·다른 파일의 함수는 안 된다.
44// ---------------------------------------------------------------------------
45
46function redraw($: EngineInterface): void {
47  $.ui.invalidate('ui.render')
48}
49
50/** PATH의 claude가 mods를 지원하는 2.1.287 이상인지 확인한다. */
51async function checkCliVersion(
52  $: EngineInterface,
53): Promise<{ ok: true } | { ok: false; message: string }> {
54  try {
55    const result = await $.process.run(['claude', '--version'], { timeoutMs: 10_000 })
56    const version = firstVersionToken(result.stdout)
57    if (version === null) {
58      return { ok: false, message: 'PATH의 claude 명령에서 버전을 읽지 못했어요.' }
59    }
60    if (!isVersionAtLeast(version, MIN_CLAUDE_VERSION)) {
61      return {
62        ok: false,
63        message: `PATH의 claude(${version})가 너무 오래됐어요. ${MIN_CLAUDE_VERSION} 이상이 필요해요.`,
64      }
65    }
66    return { ok: true }
67  } catch (error) {
68    return { ok: false, message: `claude 명령을 실행하지 못했어요: ${messageOf(error)}` }
69  }
70}
71
72/** enabledPlugins에서 설치 상태를 다시 읽는다. 패널을 열 때, 설치·제거 뒤에 부른다. */
73async function refreshInstalled($: EngineInterface): Promise<void> {
74  try {
75    const settings = await $.settings.read()
76    installed = installedNamesFrom(settings, CATALOG)
77  } catch {
78    // 설정을 못 읽으면 이전 상태를 그대로 둔다
79  }
80}
81
82async function installMod($: EngineInterface, name: string): Promise<void> {
83  if (busy.has(name)) return
84  busy = new Set(busy).add(name)
85  banner = { kind: 'none' }
86  redraw($)
87
88  try {
89    const version = await checkCliVersion($)
90    if (!version.ok) {
91      banner = errorBannerOf(installArgsFor(name), version.message)
92      return
93    }
94
95    const argv = installArgsFor(name)
96    const result = await $.process.run(argv, { timeoutMs: 60_000 })
97    const outcome = parseInstallOutcome(result)
98
99    if (outcome.kind === 'marketplace-missing') {
100      banner = { kind: 'no-marketplace', addCommand: MARKETPLACE_ADD_COMMAND }
101    } else if (outcome.kind === 'failed') {
102      banner = errorBannerOf(argv, outcome.message)
103    } else {
104      installed = new Set(installed).add(name)
105      justInstalled = name
106      $.ui.toast('설치했어요')
107    }
108  } catch (error) {
109    banner = errorBannerOf(installArgsFor(name), messageOf(error))
110  } finally {
111    const nextBusy = new Set(busy)
112    nextBusy.delete(name)
113    busy = nextBusy
114    redraw($)
115  }
116}
117
118async function uninstallMod($: EngineInterface, name: string): Promise<void> {
119  if (busy.has(name)) return
120  busy = new Set(busy).add(name)
121  banner = { kind: 'none' }
122  redraw($)
123
124  try {
125    const version = await checkCliVersion($)
126    if (!version.ok) {
127      banner = errorBannerOf(uninstallArgsFor(name), version.message)
128      return
129    }
130
131    const argv = uninstallArgsFor(name)
132    const result = await $.process.run(argv, { timeoutMs: 60_000 })
133    const outcome = parseUninstallOutcome(result)
134
135    if (outcome.kind === 'failed') {
136      banner = errorBannerOf(argv, outcome.message)
137    } else {
138      const nextInstalled = new Set(installed)
139      nextInstalled.delete(name)
140      installed = nextInstalled
141      if (justInstalled === name) justInstalled = null
142      $.ui.toast('제거했어요')
143    }
144  } catch (error) {
145    banner = errorBannerOf(uninstallArgsFor(name), messageOf(error))
146  } finally {
147    const nextBusy = new Set(busy)
148    nextBusy.delete(name)
149    busy = nextBusy
150    redraw($)
151  }
152}
153
154/** 제거는 되돌리기 성가신 일이라 $.ui.ask로 한 번 확인한다. */
155async function confirmAndUninstall($: EngineInterface, name: string): Promise<void> {
156  let answer = '취소'
157  try {
158    answer = await $.ui.ask(`${name} 모드를 제거할까요?`, ['제거', '취소'])
159  } catch {
160    return
161  }
162  if (answer !== '제거') return
163  await uninstallMod($, name)
164}
165
166async function applyReload($: EngineInterface): Promise<void> {
167  try {
168    await $.command.run({ command: 'reload-plugins', args: '' })
169    justInstalled = null
170    redraw($)
171  } catch {
172    $.ui.toast('자동 적용에 실패했어요. /reload-plugins 를 입력해 주세요.')
173  }
174}
175
176async function copyToClipboard(
177  $: EngineInterface,
178  text: string,
179  surface: Parameters<EngineInterface['ui']['copy']>[0]['surface'],
180): Promise<void> {
181  try {
182    const result = await $.ui.copy({ text, surface })
183    if (!result.isCopied) $.ui.toast('복사하지 못했어요. 직접 입력해 주세요.')
184  } catch {
185    $.ui.toast('복사하지 못했어요. 직접 입력해 주세요.')
186  }
187}
188
189// ---------------------------------------------------------------------------
190// register
191// ---------------------------------------------------------------------------
192
193export function register(on: On) {
194  on('session.start', async ($, e, next) => {
195    try {
196      await $.command.register({
197        name: COMMAND_NAME,
198        description: 'k-mods 모드 상점을 열어요',
199        immediate: true,
200      })
201    } catch {
202      // 이름이 이미 다른 곳에서 쓰이고 있으면 조용히 넘어간다
203    }
204
205    try {
206      const welcomed = await $.store.get('welcomed')
207      if (welcomed !== true) {
208        $.ui.toast('k-mods 설치 완료 · /k-mods 로 모드를 둘러보세요')
209        await $.store.set('welcomed', true)
210      }
211    } catch {
212      // 저장소를 못 읽어도 세션은 그대로 시작한다
213    }
214
215    return next(e)
216  })
217
218  on('command.run', { command: COMMAND_NAME }, async $ => {
219    const panes = await $.ui.panes()
220    const isOpen = panes.some(pane => pane.id === PANE_ID)
221
222    if (isOpen) {
223      await $.ui.close({ id: PANE_ID })
224      return {}
225    }
226
227    await refreshInstalled($)
228    await $.ui.open({ id: PANE_ID, title: PANE_TITLE, focus: true, closeOnEscape: true })
229    return {}
230  })
231
232  on('ui.render', { component: 'Pane' }, async ($, e, next) => {
233    if (e.requestId !== PANE_ID) return next(e)
234
235    const { Box, Text, Button, Link } = $.ui.resolve(e)
236    const tier = widthTierOf(e.props.bodyColumns)
237
238    const model: StoreModel = { tab, expanded, installed, busy, banner, justInstalled, tier }
239
240    const actions: StoreActions = {
241      setTab: pickedTab => {
242        tab = pickedTab
243        redraw($)
244      },
245      toggleDetail: name => {
246        expanded = expanded === name ? null : name
247        redraw($)
248      },
249      // install/uninstall/applyReload/copyCommand는 Promise를 그대로 돌려준다:
250      // 엔진과 테스트 둘 다 ui.press가 onPress의 반환값을 기다려 주므로, 눌렀을 때
251      // 비동기 작업이 끝난 뒤의 상태를 바로 이어서 확인할 수 있다.
252      install: name => installMod($, name),
253      uninstall: name => confirmAndUninstall($, name),
254      applyReload: () => applyReload($),
255      copyCommand: (text, surface) => copyToClipboard($, text, surface),
256      dismissBanner: () => {
257        banner = { kind: 'none' }
258        redraw($)
259      },
260    }
261
262    return paneView({ Box, Text, Button, Link }, CATALOG_VISIBLE, model, actions)
263  })
264}
265
hooks/catalog.ts 1602 lines
1// 생성 파일: node scripts/build.mjs 가 registry/*.json 으로 만든다. 직접 고치지 말 것.
2
3export type CategoryId = 'korean' | 'look' | 'fun' | 'meter' | 'safety' | 'workflow' | 'integration' | 'bundle'
4export type Kind = 'upstream' | 'patched' | 'original' | 'bundle'
5export type CatalogEntry = {
6  name: string
7  displayName: string
8  summary: string
9  category: CategoryId
10  kind: Kind
11  author: string
12  license: string
13  homepage?: string
14  commands: string[]
15  requires: string[]
16  notes: string[]
17  permissions: string[]
18  featured?: boolean
19  dependencies?: string[]
20}
21
22export const MARKETPLACE = 'k-mods'
23export const CATALOG_VERSION = '0.2.0'
24
25// 상점 자신은 목록에서 숨긴다
26export const SELF_NAME = 'mod-store'
27
28// 권한 라벨, 민감한 것부터
29export const PERMISSION_ORDER: readonly string[] = ["네트워크","프로그램 실행","파일 쓰기","모델 호출","도구 호출 제어","프롬프트 입력","대화 읽기","환경·설정 읽기","파일 읽기","세션 메시지","소리","화면만"]
30
31export const CATEGORIES: { id: CategoryId; label: string }[] = [
32  {
33    "id": "localize",
34    "label": "한국어화"
35  },
36  {
37    "id": "theme",
38    "label": "테마"
39  },
40  {
41    "id": "animation",
42    "label": "애니메이션"
43  },
44  {
45    "id": "statusline",
46    "label": "상태줄"
47  },
48  {
49    "id": "tool",
50    "label": "도구"
51  },
52  {
53    "id": "automation",
54    "label": "자동화"
55  },
56  {
57    "id": "guard",
58    "label": "지킴이"
59  },
60  {
61    "id": "integration",
62    "label": "연동"
63  },
64  {
65    "id": "bundle",
66    "label": "묶음"
67  }
68]
69
70export const CATALOG: CatalogEntry[] = [
71  {
72    "name": "ko-ui",
73    "displayName": "한국어 UI 번역 사전",
74    "summary": "슬래시 커맨드 설명, `/config` 항목, 작업 표시줄 같은 화면 문구를 번역 사전에 맞춰 한국어로 표시해요.",
75    "category": "localize",
76    "kind": "upstream",
77    "author": "moduvoice",
78    "license": "MIT",
79    "homepage": "https://github.com/moduvoice/claude-code-ko-ui",
80    "commands": [
81      "/ko-dump"
82    ],
83    "requires": [],
84    "notes": [
85      "Claude Code 2.1.287 이상이 필요해요. 2.1.285에서는 `CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1` 환경 변수를 켜야 로드돼요.",
86      "모델 호출·네트워크 요청이 전혀 없는 고정 사전(dict.json) 치환 방식이라 토큰을 쓰지 않아요.",
87      "Spinner 단어 번역은 terminal 화면에서만 적용돼요(desktop은 원문 유지, 나머지 번역은 desktop에서도 적용돼요).",
88      "번역 안 된 새 문구는 `/ko-dump`로 확인해 `dict.json`에 직접 추가할 수 있어요(최대 200개까지 기록).",
89      "권한 확인 창, 환영 화면, 선택지 값, 에러 메시지, 프롬프트 아래 알약 문구는 훅이 없거나 번역하면 다른 정보가 사라져서 원문 그대로예요."
90    ],
91    "permissions": [
92      "대화 읽기",
93      "파일 읽기"
94    ],
95    "featured": true
96  },
97  {
98    "name": "skins",
99    "displayName": "스킨 (한국 수정판)",
100    "summary": "도구 호출·표·코드·셸 출력을 테마 카드 형식으로 표시해요. 원본에서 한글 표가 잘리던 문제를 고쳤어요",
101    "category": "theme",
102    "kind": "patched",
103    "author": "hellosverre",
104    "license": "MIT",
105    "homepage": "https://github.com/SeongGwangJu/k-mods/tree/main/mods/skins",
106    "commands": [
107      "/skin"
108    ],
109    "requires": [],
110    "notes": [],
111    "permissions": [
112      "도구 호출 제어",
113      "대화 읽기",
114      "환경·설정 읽기"
115    ],
116    "featured": true
117  },
118  {
119    "name": "gfm-render",
120    "displayName": "GFM 렌더러",
121    "summary": "대화 기록에 있는 GFM 경고문·체크리스트·취소선·Mermaid 다이어그램을 화면에 표시해요.",
122    "category": "theme",
123    "kind": "upstream",
124    "author": "briangtn",
125    "license": "MIT",
126    "homepage": "https://github.com/briangtn/claude-gfm-render",
127    "commands": [],
128    "requires": [],
129    "notes": [],
130    "permissions": [
131      "프로그램 실행",
132      "대화 읽기"
133    ]
134  },
135  {
136    "name": "prismantis",
137    "displayName": "프리즈만티스",
138    "summary": "표·코드·다이어그램·도구 호출 결과를 15가지 테마로 표시하고 복사 버튼을 추가해요.",
139    "category": "theme",
140    "kind": "upstream",
141    "author": "Nahum Litvin",
142    "license": "MIT",
143    "homepage": "https://github.com/NahumLitvin/prismantis",
144    "commands": [
145      "/prismantis"
146    ],
147    "requires": [],
148    "notes": [],
149    "permissions": [
150      "대화 읽기",
151      "환경·설정 읽기"
152    ]
153  },
154  {
155    "name": "spinner",
156    "displayName": "픽셀 장면 스피너 (한국 수정판)",
157    "summary": "Claude가 일하는 동안 입력창 위에 냥캣·Clawd 같은 픽셀 장면을 표시하고 펫을 키워요. 한국어·간결한 기본값으로 고친 수정판이에요",
158    "category": "animation",
159    "kind": "patched",
160    "author": "hoobnn",
161    "license": "MIT",
162    "homepage": "https://github.com/hoobnn/hoobnn-agent-mods",
163    "commands": [
164      "/spinner"
165    ],
166    "requires": [
167      "swiftc/Xcode Command Line Tools (오디오 테마 전용, macOS 14.2+ 및 '시스템 오디오 녹음' 권한 필요)"
168    ],
169    "notes": [
170      "원본과 다른 기본값: 한국어, 펫 줄 꺼짐, 하단 버튼 꺼짐, 스피너 줄 앞 마스코트 없음, random은 nyan·clawd·thunder·chomp 중 턴마다. 자세한 내용은 mod 설명서의 \"원본과 다른 점\"에 있어요.",
171      "`theme`를 audio로 바꾸면 첫 실행 시 `swiftc`로 `audio-tap.swift`를 로컬 컴파일해 실행하고, macOS Core Audio로 시스템 출력 소리의 레벨만 읽어 보여줘요(macOS 14.2+, 터미널에 '시스템 오디오 녹음' 권한 허용 필요). 저장·전송은 하지 않지만 Xcode Command Line Tools가 필요해요.",
172      "터미널이 느리거나 저전력 환경이면 `reducedMotion`을 켜서 마스코트·밴드·펫을 정지 이미지로 바꿀 수 있어요.",
173      "같은 작성자(hoobnn)의 `hud` 모드를 함께 쓰면 펫을 hud 쪽에 표시하고 쓰다듬은 횟수를 공유해요. `hud`가 없어도 정상 동작해요(안전한 no-op).",
174      "`language: ko`는 `/spinner` 응답과 레벨업·피날레 문구까지 전부 한국어로 번역되어 있어요(자리표시자 아님)."
175    ],
176    "permissions": [
177      "프로그램 실행",
178      "도구 호출 제어",
179      "대화 읽기",
180      "환경·설정 읽기",
181      "파일 읽기"
182    ],
183    "featured": true
184  },
185  {
186    "name": "cc-arcade",
187    "displayName": "CC 아케이드",
188    "summary": "입력창 위에서 스네이크·테트리스·2048·지뢰찾기 등 9가지 미니게임을 즐길 수 있어요. Claude가 일하는 동안 자라는 펫도 키울 수 있어요.",
189    "category": "animation",
190    "kind": "upstream",
191    "author": "Seza Akgün",
192    "license": "MIT",
193    "homepage": "https://github.com/sezaakgun/cc-arcade",
194    "commands": [
195      "/arcade"
196    ],
197    "requires": [
198      "대화형 터미널(비대화형 `-p` 모드에서는 동작하지 않아요)",
199      "Claude Code 2.1.287 미만이면 CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 환경 변수 필요"
200    ],
201    "notes": [
202      "장르를 본뜬 클론이며 Tetris Holding·Taito·Atari·id Software와 무관해요(업스트림 설명 그대로).",
203      "최고 점수와 펫 상태는 로컬 $.store에만 저장돼요.",
204      "`/arcade colorblind`로 Doom을 빨강·초록 구분 없는 팔레트로 바꿀 수 있어요."
205    ],
206    "permissions": [
207      "도구 호출 제어",
208      "대화 읽기"
209    ]
210  },
211  {
212    "name": "clawd-spinner",
213    "displayName": "Clawd 스피너",
214    "summary": "189가지 스피너 단어마다 Clawd가 요리·춤·서성거리기 등 서로 다른 몸짓을 해요.",
215    "category": "animation",
216    "kind": "upstream",
217    "author": "Sai Rudra",
218    "license": "MIT",
219    "homepage": "https://github.com/saiharsha03/clawd-spinner",
220    "commands": [
221      "/clawd-talk"
222    ],
223    "requires": [],
224    "notes": [
225      "좁은 터미널(50칸 미만)에서는 자동으로 숨고 기본 스피너만 보여요.",
226      "`/clawd-talk`로 말풍선을 껐다 켤 수 있고, 설정은 세션 간 기억돼요."
227    ],
228    "permissions": [
229      "대화 읽기"
230    ]
231  },
232  {
233    "name": "clawd-tales",
234    "displayName": "Clawd 이야기",
235    "summary": "입력창 위에서 픽셀 Clawd가 Claude의 모든 도구 호출에 맞춰 몸짓을 해요. 서브에이전트마다 작은 동료도 등장해요.",
236    "category": "animation",
237    "kind": "upstream",
238    "author": "plaxagoras",
239    "license": "MIT",
240    "homepage": "https://github.com/plaxagoras/clawd-tales",
241    "commands": [
242      "/tales"
243    ],
244    "requires": [],
245    "notes": [
246      "히어로의 걷기·환호 스프라이트 원형과 기본 팔레트는 Claude Fables(henrik-thevibe, MIT)에서 가져왔다고 NOTICE 파일에 명시돼 있어요.",
247      "서브에이전트가 뜨면 작은 동료가 따로 등장하고, 모델 색깔에 따라 외형이 달라져요.",
248      "mobile 화면에서는 그려지지 않도록 코드로 명시돼 있어요."
249    ],
250    "permissions": [
251      "도구 호출 제어",
252      "대화 읽기"
253    ]
254  },
255  {
256    "name": "combo-meter",
257    "displayName": "콤보 미터",
258    "summary": "도구 호출이 성공하면 콤보 수치와 랭크가 올라가고 실패하면 콤보가 끊겨요. 랭크는 D부터 SSS까지예요.",
259    "category": "animation",
260    "kind": "upstream",
261    "author": "Sarthak Bhatore",
262    "license": "MIT",
263    "homepage": "https://github.com/sarthak2511/claude-combo",
264    "commands": [
265      "/combo"
266    ],
267    "requires": [],
268    "notes": [],
269    "permissions": [
270      "도구 호출 제어",
271      "대화 읽기",
272      "소리"
273    ]
274  },
275  {
276    "name": "diff-invaders",
277    "displayName": "디프 인베이더",
278    "summary": "Claude가 방금 쓴 diff 줄에 대응하는 외계인 편대를 표시하는 스페이스 인베이더예요. 토큰 소모 없이 입력창 위에서 플레이해요",
279    "category": "animation",
280    "kind": "upstream",
281    "author": "claude-code-templates",
282    "license": "MIT",
283    "homepage": "https://github.com/davila7/claude-code-templates/tree/375af9018a40e330e81542f59054daaa088c21aa/cli-tool/components/mods/games/diff-invaders",
284    "commands": [
285      "/diff-invaders"
286    ],
287    "requires": [],
288    "notes": [
289      "마우스 없이 방향키(←→/ad)와 스페이스만으로 플레이 가능해요(클릭도 지원)",
290      "최고 점수만 $.store(plugin 전용 저장소)에 남기고, 그 외 기기 밖 전송은 없어요",
291      "README에 'claude -p, 데스크톱 앱, 모바일에서는 아무것도 그려지지 않는다'고 명시되어 있어요(terminal 전용)"
292    ],
293    "permissions": [
294      "도구 호출 제어",
295      "대화 읽기"
296    ]
297  },
298  {
299    "name": "reels",
300    "displayName": "릴스",
301    "summary": "Claude가 작업하는 동안 YouTube Shorts를 터미널 패널에서 재생하고 작업이 끝나면 자동으로 멈춰요. `/reels`를 입력했을 때만 재생돼요.",
302    "category": "animation",
303    "kind": "upstream",
304    "author": "Hamza Zafar",
305    "license": "MIT",
306    "homepage": "https://github.com/hamzafer/claude-code-mods/tree/main/mods/reels",
307    "commands": [
308      "/reels"
309    ],
310    "requires": [
311      "Playwright (최초 /reels 실행 시 설치 안내가 떠요: npm install --prefix <plugin> playwright)"
312    ],
313    "notes": [
314      "코드(helper/reels.cjs)까지 직접 읽어 확인했어요. Playwright로 헤드리스 Chrome을 띄워 https://www.youtube.com/shorts 를 열고, 화면을 CDP 스크린캐스트로 캡처해 PNG로 저장한 뒤 패널에 보여줘요. 제어는 로컬 유닉스 소켓(파일 경로를 쓰는 IPC)으로만 하고, 바깥에 열린 네트워크 포트는 아니에요.",
315      "프로필(쿠키 등)은 ~/.claude-mods/reels/profile에 로컬로만 저장돼요."
316    ],
317    "permissions": [
318      "네트워크",
319      "프로그램 실행",
320      "대화 읽기",
321      "환경·설정 읽기",
322      "파일 읽기"
323    ]
324  },
325  {
326    "name": "tool-defense",
327    "displayName": "툴 디펜스",
328    "summary": "Claude의 실제 도구 호출(Bash·Edit·웹·Agent)에 대응하는 적 유닛을 표시하는 타워 디펜스예요. 토큰 소모 없이 입력창 위에서 플레이해요",
329    "category": "animation",
330    "kind": "upstream",
331    "author": "claude-code-templates",
332    "license": "MIT",
333    "homepage": "https://github.com/davila7/claude-code-templates/tree/375af9018a40e330e81542f59054daaa088c21aa/cli-tool/components/mods/games/tool-defense",
334    "commands": [
335      "/defense"
336    ],
337    "requires": [],
338    "notes": [
339      "클릭(마우스)으로 타워를 짓고 업그레이드해요. 일시정지·재시작은 키보드(스페이스/p/r)로도 가능해요",
340      "최고 점수만 $.store(plugin 전용 저장소)에 남기고, 그 외 기기 밖 전송은 없어요",
341      "README에 'claude -p, 데스크톱 앱, 모바일에서는 아무것도 그려지지 않는다'고 명시되어 있어요(마우스 가능한 terminal 전용)"
342    ],
343    "permissions": [
344      "도구 호출 제어",
345      "대화 읽기"
346    ]
347  },
348  {
349    "name": "status-ko",
350    "displayName": "작업 상태 한국어",
351    "summary": "작업 중에는 '읽는 중 · page.tsx'처럼 지금 하는 일을 보여줘요. 끝나면 모델·시간·도구·캐시를 한 줄로 정리해요",
352    "category": "statusline",
353    "kind": "original",
354    "author": "SeongGwangJu",
355    "license": "MIT",
356    "homepage": "https://github.com/SeongGwangJu/k-mods/tree/main/mods/status-ko",
357    "commands": [],
358    "requires": [],
359    "notes": [],
360    "permissions": [
361      "도구 호출 제어",
362      "대화 읽기"
363    ],
364    "featured": true
365  },
366  {
367    "name": "ctx-strip",
368    "displayName": "컨텍스트 막대",
369    "summary": "컨텍스트 구성(대화·도구·스킬…)을 입력창 위 막대로 표시해요. 서브에이전트가 실행되면 한 줄로 알려줘요",
370    "category": "statusline",
371    "kind": "original",
372    "author": "SeongGwangJu",
373    "license": "MIT",
374    "homepage": "https://github.com/SeongGwangJu/k-mods/tree/main/mods/ctx-strip",
375    "commands": [
376      "/ctx"
377    ],
378    "requires": [],
379    "notes": [],
380    "permissions": [
381      "프로그램 실행",
382      "파일 쓰기",
383      "도구 호출 제어",
384      "대화 읽기",
385      "환경·설정 읽기"
386    ]
387  },
388  {
389    "name": "agent-radar",
390    "displayName": "에이전트 레이더",
391    "summary": "실행 중인 서브에이전트마다 경과 시간·도구 호출 수·현재 작업을 입력창 위 한 줄로 표시해요. /radar로 전체 목록과 대화 내용을 확인할 수 있어요.",
392    "category": "statusline",
393    "kind": "upstream",
394    "author": "Hamza Zafar",
395    "license": "MIT",
396    "homepage": "https://github.com/hamzafer/claude-code-mods/tree/main/mods/agent-radar",
397    "commands": [
398      "/radar"
399    ],
400    "requires": [],
401    "notes": [
402      "같은 저장소의 mission-control도 서브에이전트를 추적하지만 코드맵까지 포함한 더 무거운 패널이에요. agent-radar는 가볍고 단일 목적(에이전트 상태)이라 함께 설치해도 역할이 겹치지 않는다고 판단했어요."
403    ],
404    "permissions": [
405      "도구 호출 제어",
406      "대화 읽기"
407    ]
408  },
409  {
410    "name": "browser-lanes",
411    "displayName": "브라우저 레인",
412    "summary": "이 세션에 Playwright 브라우저가 있는지, 현재 누가 쓰는지 입력창 위에 표시해요. 서브에이전트끼리는 차례대로 사용하게 해요.",
413    "category": "statusline",
414    "kind": "upstream",
415    "author": "Hamza Zafar",
416    "license": "MIT",
417    "homepage": "https://github.com/hamzafer/claude-code-mods/tree/main/mods/browser-lanes",
418    "commands": [
419      "/browser"
420    ],
421    "requires": [
422      "Playwright MCP 서버 (claude mcp add playwright --scope user -- npx @playwright/mcp@latest --isolated 권장)"
423    ],
424    "notes": [
425      "--isolated 없이 Playwright MCP를 쓰면 세션마다 Chrome 프로필을 공유해 두 번째 세션이 'Browser is already in use' 오류를 겪는데, 이 mod는 그 상황을 알려주고 /browser clean으로 정리해 주는 역할이에요. 근본 해결은 --isolated 설정이라고 README에도 명시돼 있어요."
426    ],
427    "permissions": [
428      "프로그램 실행",
429      "도구 호출 제어",
430      "대화 읽기"
431    ]
432  },
433  {
434    "name": "cache-panel",
435    "displayName": "캐시 패널",
436    "summary": "프롬프트 캐시가 만료되기 50분 전에 알려줘요. 주기적 갱신, 한 번 갱신, 대화 압축 중 하나를 예상 비용과 함께 선택할 수 있어요.",
437    "category": "statusline",
438    "kind": "upstream",
439    "author": "Dustin Yuchen Teng",
440    "license": "MIT",
441    "homepage": "https://github.com/danyuchn/claude-mods",
442    "commands": [
443      "/warm"
444    ],
445    "requires": [],
446    "notes": [],
447    "permissions": [
448      "프로그램 실행",
449      "모델 호출",
450      "환경·설정 읽기"
451    ]
452  },
453  {
454    "name": "context-bar",
455    "displayName": "컨텍스트 바 (hamzafer)",
456    "summary": "컨텍스트 창을 /context와 같은 색의 구간별 막대 그래프로 표시해요. 토큰 수·압축 시점·범례도 입력창 위에서 확인할 수 있어요.",
457    "category": "statusline",
458    "kind": "upstream",
459    "author": "Hamza Zafar",
460    "license": "MIT",
461    "homepage": "https://github.com/hamzafer/claude-code-mods/tree/main/mods/context-bar",
462    "commands": [
463      "/context-bar"
464    ],
465    "requires": [],
466    "notes": [
467      "k-mods 자체 mod인 ctx-strip과 개념이 겹쳐요(둘 다 컨텍스트 창을 막대로 보여줌). 이름은 다르므로(string 충돌 아님) 등록은 했지만, 기계적으로는 ctx-strip이 '막대 + 서브에이전트 한 줄'을 한 번에 보여주는 반면 context-bar는 /context와 동일한 카테고리별 색상·범례·압축 임계값 표시에 집중한 순수 컨텍스트 전용 버전이라는 차이가 있어요. 최종적으로 사용자에게 둘 다 보여줄지, 하나만 추천할지는 교차 조율이 필요해 보여요."
468    ],
469    "permissions": [
470      "대화 읽기"
471    ]
472  },
473  {
474    "name": "flightdeck",
475    "displayName": "플라이트덱 대시보드",
476    "summary": "메인 모델의 상태·비용·컨텍스트, 온콜 아키텍트 상담, 권한 검사, 서브에이전트 카드를 한 화면에 표시해요. 세션 이벤트가 발생하면 실시간으로 갱신해요",
477    "category": "statusline",
478    "kind": "upstream",
479    "author": "Stephen Casella",
480    "license": "MIT",
481    "homepage": "https://github.com/scasella/claude-flightdeck",
482    "commands": [
483      "/flightdeck"
484    ],
485    "requires": [],
486    "notes": [],
487    "permissions": [
488      "도구 호출 제어",
489      "대화 읽기"
490    ]
491  },
492  {
493    "name": "hud",
494    "displayName": "claude-hud 상태 대시보드",
495    "summary": "모델·프로젝트·Git·컨텍스트·사용량·도구·할 일을 HUD 한 줄로 표시해요. 예산·이력·작업 요약·상세 패널·테마도 지원해요.",
496    "category": "statusline",
497    "kind": "upstream",
498    "author": "hoobnn",
499    "license": "MIT",
500    "homepage": "https://github.com/hoobnn/hoobnn-agent-mods/tree/8fb6f67cad9fd023cc7c06b7923518b4bbc45aea/claude-code/hud",
501    "commands": [
502      "/hud"
503    ],
504    "requires": [
505      "Nerd Font (nerd·powerline 테마에서만 필요)"
506    ],
507    "notes": [
508      "[jarrodwatts/claude-hud](https://github.com/jarrodwatts/claude-hud) 0.10.0을 mod로 옮긴 것으로, 별도 LICENSE.claude-hud(MIT, Jarrod Watts)로 원저작권을 유지해요. 화면 언어는 k-mods의 `/config`가 아니라 claude-hud 자신의 설정 파일(`~/.claude/plugins/claude-hud/config.json` 또는 `~/.claude/claude-hud.json`)의 `language` 필드를 따라요(기본값 en). plugin.json userConfig에 language 옵션 자체가 없어 여기서는 language 설정을 권장하지 않았어요.",
509      "`summaryEveryTurns`(기본 5턴, 0으로 끌 수 있음)마다 세션 작업을 한 줄로 요약하는 모델 호출이 있어요. 대화를 포크해 프롬프트 캐시를 재사용하는 호출이라 비용은 적지만 사용자 사용량을 소모해요.",
510      "`extraCmd`(claude-hud의 --extra-cmd, 임의 쉘 명령 실행)는 기본 빈 값이고, 쓰려면 옵션 설정과 `CLAUDE_HUD_ALLOW_EXTRA_CMD=1` 환경 변수를 모두 켜야 해요.",
511      "계정 인증 방식·이메일 일부를 보여주는 기능(claude-hud의 showAuth/showAuthUser)과 다른 로컬 도구로 사용량을 공유하는 파일(externalUsagePath/externalUsageWritePath)은 claude-hud 자체 설정 파일에서만 켤 수 있고 둘 다 기본값이 꺼짐이에요.",
512      "같은 작성자(hoobnn)의 `spinner` mod를 함께 설치하면 pet이 HUD 옆(below 위치일 때)에 표시돼요. spinner가 없어도 정상 동작해요."
513    ],
514    "permissions": [
515      "프로그램 실행",
516      "파일 쓰기",
517      "모델 호출",
518      "도구 호출 제어",
519      "대화 읽기",
520      "환경·설정 읽기",
521      "파일 읽기",
522      "소리"
523    ]
524  },
525  {
526    "name": "mod-usage",
527    "displayName": "사용량 진행률 막대",
528    "summary": "컨텍스트·5시간·7일 사용량을 입력창 위 그라데이션 막대 3개로 표시해요. Desktop/VS Code 전용이며 12개 언어를 지원하지만 한국어는 아직 없어요.",
529    "category": "statusline",
530    "kind": "upstream",
531    "author": "Jack Chiang",
532    "license": "MIT",
533    "homepage": "https://github.com/jack21/claude-mod-usage",
534    "commands": [],
535    "requires": [],
536    "notes": [
537      "한국어는 아직 지원 언어가 아니에요. 한국어 로캘에서도 영어로 표시돼요.",
538      "한국어 로캘(`ko_KR` 등)이어도 지원 목록 12개 언어에 `ko`가 없어서 자동으로 영어로 표시돼요. 번역 패치가 반영되기 전까지는 `/plugin configure`의 언어 선택지에도 한국어가 없어요.",
539      "터미널에서는 그려지지 않아요. Claude Code Desktop/VS Code/모바일 전용이고, CLI는 자체 상태 표시줄을 그대로 써요.",
540      "5시간·7일 막대는 Claude 구독 사용자에게만 보여요. API 키로 쓰면 컨텍스트 막대만 보여요 (원본 README 기준, k-mods가 직접 확인한 건 아니에요).",
541      "macOS에서 `LANG`류 환경변수가 하나도 없을 때만 내부적으로 `/usr/bin/defaults read -g AppleLanguages`를 1회 실행해 시스템 언어를 읽어요. 인자가 고정돼 있고 사용자 입력은 들어가지 않아요."
542    ],
543    "permissions": [
544      "프로그램 실행",
545      "환경·설정 읽기"
546    ]
547  },
548  {
549    "name": "pr-pulse",
550    "displayName": "PR 펄스",
551    "summary": "GitHub PR의 머지 준비 상태·CI 체크·리뷰 코멘트·리뷰 대기열을 입력창 위 한 줄과 패널에 실시간으로 표시해요",
552    "category": "statusline",
553    "kind": "upstream",
554    "author": "Gerric Chaplin",
555    "license": "MIT",
556    "homepage": "https://github.com/gerricchaplin/pr-pulse",
557    "commands": [
558      "/pulse"
559    ],
560    "requires": [
561      "gh CLI 로그인 (gh auth status)"
562    ],
563    "notes": [
564      "GitHub API 호출이 전부 로컬 gh 인증을 그대로 써요. 다른 작업과 시간당 한도를 같이 써요 (15초/60초 주기로 자동 새로고침)."
565    ],
566    "permissions": [
567      "프로그램 실행",
568      "프롬프트 입력"
569    ]
570  },
571  {
572    "name": "prompt-cache-control",
573    "displayName": "프롬프트 캐시 미터",
574    "summary": "요청마다 캐시를 읽고 새로 쓴 양을 입력창 위에 표시해요. 만료가 임박하면 알리고 /compact·/clear 시점을 제안해요",
575    "category": "statusline",
576    "kind": "upstream",
577    "author": "claude-code-templates",
578    "license": "MIT",
579    "homepage": "https://github.com/davila7/claude-code-templates/tree/375af9018a40e330e81542f59054daaa088c21aa/cli-tool/components/mods/observability/prompt-cache-control",
580    "commands": [
581      "/cache"
582    ],
583    "requires": [],
584    "notes": [
585      "TTL(캐시 유지 시간)은 환경변수·설정·계정 종류(구독/API 키)로 자동 추정되고, 요청 간격을 관찰해 스스로 보정해요",
586      "1초 간격 타이머가 돌지만 표시 내용이 바뀔 때만 다시 그려서 유휴 세션엔 비용이 없어요"
587    ],
588    "permissions": [
589      "환경·설정 읽기",
590      "파일 읽기"
591    ]
592  },
593  {
594    "name": "receipt",
595    "displayName": "턴 영수증",
596    "summary": "매 턴이 끝나면 바뀐 파일·실행한 명령·읽은 횟수를 입력창 위 한 줄 요약으로 표시해요. 같은 작업을 반복하면 알려줘요.",
597    "category": "statusline",
598    "kind": "upstream",
599    "author": "hoobnn",
600    "license": "MIT",
601    "homepage": "https://github.com/hoobnn/hoobnn-agent-mods/tree/8fb6f67cad9fd023cc7c06b7923518b4bbc45aea/claude-code/receipt",
602    "commands": [
603      "/receipt"
604    ],
605    "requires": [],
606    "notes": [
607      "같은 작성자의 `hud`·`ts-band`·`todo-bar`·`hitokoto`와 같은 Box/Text 기반 렌더링을 쓰지만, 이 mod의 테스트 스위트만 유일하게 terminal 서피스만 돌려 확인해 surfaces를 terminal로만 표기했어요(desktop이 안 되는 것을 확인한 건 아니고, 작성자가 desktop으로 검증한 기록이 없다는 뜻이에요).",
608      "채팅만 하고 도구를 쓰지 않은 턴은 영수증을 남기지 않고, 다음 턴이 시작되면 자동으로 접혀요."
609    ],
610    "permissions": [
611      "도구 호출 제어",
612      "대화 읽기",
613      "환경·설정 읽기"
614    ]
615  },
616  {
617    "name": "review-watch",
618    "displayName": "리뷰 워치",
619    "summary": "실행 중인 codex review나 '리뷰' 서브에이전트마다 모델·대상·경과 시간을 한 줄로 표시해요. 끝나면 발견 항목 수를 화면 알림으로 알려줘요.",
620    "category": "statusline",
621    "kind": "upstream",
622    "author": "Hamza Zafar",
623    "license": "MIT",
624    "homepage": "https://github.com/hamzafer/claude-code-mods/tree/main/mods/review-watch",
625    "commands": [],
626    "requires": [
627      "ps, tail (macOS/Linux 기본 제공)",
628      "Codex CLI (선택. codex review 추적 시에만 의미 있음)"
629    ],
630    "notes": [
631      "저장소 설명에 'codex review 감시' 기능이 있다고 해서 codex 바이너리를 직접 실행하는지 코드로 확인했어요. 직접 실행하지 않아요. Claude가 이미 Bash로 실행한 `codex review` 명령을 ps/tail로 관찰만 하고, 실행 여부 결정이나 차단은 merge-gate(같은 저장소)의 역할이에요."
632    ],
633    "permissions": [
634      "프로그램 실행",
635      "도구 호출 제어",
636      "대화 읽기",
637      "환경·설정 읽기"
638    ]
639  },
640  {
641    "name": "statuspane",
642    "displayName": "상태 카드",
643    "summary": "모델, 이펙트, 컨텍스트, 5시간·주간 한도, 비용, 브랜치를 프롬프트 위 카드 하나에 표시해요.",
644    "category": "statusline",
645    "kind": "upstream",
646    "author": "Anji Xu",
647    "license": "MIT",
648    "homepage": "https://github.com/xuanji86/claude-statuspane",
649    "commands": [
650      "/statuspane"
651    ],
652    "requires": [
653      "gh CLI (ciBranch 또는 ciPush 설정을 켰을 때만 필요해요, 기본은 둘 다 꺼져 있어요)"
654    ],
655    "notes": [
656      "데스크톱 앱에서는 모델·이펙트·컨텍스트를 자체적으로 보여주기 때문에 이 카드는 터미널에서만 그려져요.",
657      "Claude Code 2.1.287 이상 필요, 2.1.288에서 테스트됐다고 README에 적혀 있어요."
658    ],
659    "permissions": [
660      "프로그램 실행",
661      "도구 호출 제어",
662      "대화 읽기",
663      "환경·설정 읽기",
664      "파일 읽기"
665    ]
666  },
667  {
668    "name": "taxi-meter",
669    "displayName": "택시 미터기",
670    "summary": "입력창 위 택시 미터기 패널에 세션 요금과 5시간·주간 한도를 표시해요. /meter·/receipt로 자세히 볼 수 있어요.",
671    "category": "statusline",
672    "kind": "upstream",
673    "author": "개발동생 (devbrothers)",
674    "license": "MIT",
675    "homepage": "https://github.com/devbrother2024/devbrothers-mods",
676    "commands": [
677      "/meter",
678      "/receipt"
679    ],
680    "requires": [],
681    "notes": [],
682    "permissions": [
683      "대화 읽기",
684      "소리"
685    ]
686  },
687  {
688    "name": "todo-bar",
689    "displayName": "할 일 진행 상태줄",
690    "summary": "Claude가 만든 할 일 목록의 진행 상황과 경과 시간을 입력창 위 막대로 표시해요.",
691    "category": "statusline",
692    "kind": "upstream",
693    "author": "hoobnn",
694    "license": "MIT",
695    "homepage": "https://github.com/hoobnn/hoobnn-agent-mods/tree/8fb6f67cad9fd023cc7c06b7923518b4bbc45aea/claude-code/todo-bar",
696    "commands": [
697      "/todos"
698    ],
699    "requires": [],
700    "notes": [
701      "TodoWrite·TaskCreate·TaskUpdate 결과만 사후에 읽어서 그리고, 별도 도구를 등록하거나 프롬프트에 끼어들지 않아 토큰을 쓰지 않아요.",
702      "거부되거나 실패한 호출, 서브에이전트 자신의 할 일 목록은 집계하지 않아요.",
703      "세션별 진행 상황은 최근 20개 세션까지 로컬에 보관되어 세션을 재개해도 이어서 보여요."
704    ],
705    "permissions": [
706      "도구 호출 제어",
707      "대화 읽기",
708      "환경·설정 읽기"
709    ]
710  },
711  {
712    "name": "token-weather",
713    "displayName": "토큰 날씨",
714    "summary": "컨텍스트 사용량을 날씨 아이콘과 최근 턴 막대그래프로 입력창 위에 표시해요.",
715    "category": "statusline",
716    "kind": "upstream",
717    "author": "Claude Code DevRel",
718    "license": "Apache-2.0",
719    "homepage": "https://github.com/anthropics/claude-code-playground/tree/main/claude-code/mods/token-weather",
720    "commands": [],
721    "requires": [],
722    "notes": [
723      "턴이 끝날 때마다 `$.session.usage()`로 컨텍스트 사용률만 다시 읽어요. 추가 네트워크·모델 호출 없어요.",
724      "최근 12번 턴의 토큰 수를 막대그래프로 보여줘요."
725    ],
726    "permissions": [
727      "대화 읽기"
728    ]
729  },
730  {
731    "name": "ts-band",
732    "displayName": "테일스케일 노드 상태줄",
733    "summary": "입력창 위에 Tailscale 노드들의 연결 상태를 표시해요. 노드가 끊기거나 다시 연결되면 화면 알림으로 알려줘요.",
734    "category": "statusline",
735    "kind": "upstream",
736    "author": "hoobnn",
737    "license": "MIT",
738    "homepage": "https://github.com/hoobnn/hoobnn-agent-mods/tree/8fb6f67cad9fd023cc7c06b7923518b4bbc45aea/claude-code/ts-band",
739    "commands": [
740      "/ts"
741    ],
742    "requires": [
743      "tailscale CLI"
744    ],
745    "notes": [
746      "Tailscale이 설치되어 있어야 동작해요(없으면 '찾을 수 없음' 오류만 조용히 표시).",
747      "기본 새로고침 주기는 60초(최소 10초)이고, 조회 실패가 이어지면 최대 10분까지 점점 느리게 재시도해요.",
748      "`tailscalePath`를 비워두면 PATH, Homebrew 설치 경로, macOS 앱 내장 CLI 순서로 자동으로 찾아요."
749    ],
750    "permissions": [
751      "프로그램 실행",
752      "도구 호출 제어",
753      "대화 읽기",
754      "환경·설정 읽기"
755    ]
756  },
757  {
758    "name": "usage-band-pawandeep",
759    "displayName": "사용량 밴드 (5시간·주간)",
760    "summary": "입력창 위에 5시간·주간 사용량 퍼센트와 초기화 카운트다운을 항상 표시해요. 새 채팅·GitHub 푸시 버튼도 함께 제공해요.",
761    "category": "statusline",
762    "kind": "upstream",
763    "author": "Pawandeep",
764    "license": "MIT",
765    "homepage": "https://github.com/pawandeepdhall/claude-mods#usage-band",
766    "commands": [],
767    "requires": [],
768    "notes": [
769      "↑ 푸시 버튼은 git 명령을 직접 실행하지 않아요. 커밋 후 푸시해 달라는 요청을 사용자가 직접 입력한 것처럼 Claude에게 제출할 뿐이라, 실제 git 실행은 평소 권한 설정(permission)을 그대로 따라요.",
770      "+ 새 채팅 버튼은 Windows에서는 번들된 VBScript(wscript.exe)로, macOS에서는 osascript로 Ctrl+N/Cmd+N 키 입력을 흉내 내요.",
771      "같은 저장소의 `next-steps` 플러그인은 이번 심사 대상이 아니에요(모델 호출 `$.model.complete`을 써서 별도 검토가 필요해요).",
772      "비슷한 성격의 사용량 표시 mod로 `mod-usage`(Desktop/VS Code 전용, 다국어)와 `taxi-meter`(택시 미터기 테마, 원화 환산)도 등록돼 있어요. 이 mod는 터미널·데스크톱 모두 되고 새 채팅·GitHub 푸시 버튼이 있다는 점이 달라요."
773    ],
774    "permissions": [
775      "프로그램 실행",
776      "프롬프트 입력"
777    ]
778  },
779  {
780    "name": "mod-store",
781    "displayName": "모드 상점",
782    "summary": "`/k-mods`를 입력하면 카탈로그가 열려요. 버튼으로 mod를 설치하거나 제거할 수 있어요",
783    "category": "tool",
784    "kind": "original",
785    "author": "SeongGwangJu",
786    "license": "MIT",
787    "homepage": "https://github.com/SeongGwangJu/k-mods/tree/main/mods/mod-store",
788    "commands": [
789      "/k-mods"
790    ],
791    "requires": [],
792    "notes": [],
793    "permissions": [
794      "프로그램 실행",
795      "환경·설정 읽기"
796    ],
797    "featured": true
798  },
799  {
800    "name": "memo-pad",
801    "displayName": "메모장",
802    "summary": "Claude가 일하는 동안 다음에 시킬 일을 적어 두고 버튼 한 번으로 입력창에 추가해요",
803    "category": "tool",
804    "kind": "original",
805    "author": "SeongGwangJu",
806    "license": "MIT",
807    "homepage": "https://github.com/SeongGwangJu/k-mods/tree/main/mods/memo-pad",
808    "commands": [
809      "/m"
810    ],
811    "requires": [],
812    "notes": [],
813    "permissions": [
814      "프롬프트 입력"
815    ]
816  },
817  {
818    "name": "agent-flow",
819    "displayName": "에이전트 플로우",
820    "summary": "메인 루프와 서브에이전트의 관계를 트리 구조로 표시해요. 각 에이전트에 오간 컨텍스트 양과 답변은 클릭해서 볼 수 있어요",
821    "category": "tool",
822    "kind": "upstream",
823    "author": "claude-code-templates",
824    "license": "MIT",
825    "homepage": "https://github.com/davila7/claude-code-templates/tree/375af9018a40e330e81542f59054daaa088c21aa/cli-tool/components/mods/ui/agent-flow",
826    "commands": [
827      "/agent-flow"
828    ],
829    "requires": [],
830    "notes": [
831      "데스크톱 앱에서의 동작은 README·코드에 명시적 확인이 없어 이번 리뷰에서는 terminal만 표시했어요(테스트도 terminal surface 기준)",
832      "순수 관찰용 mod예요. 모든 훅이 next(e) 결과를 그대로 통과시키고 $.agent.list·$.session.usage만 추가로 읽어요"
833    ],
834    "permissions": [
835      "도구 호출 제어",
836      "대화 읽기"
837    ]
838  },
839  {
840    "name": "agent-quick-menu",
841    "displayName": "퀵 메뉴",
842    "summary": "설치된 플러그인의 명령과 설정을 한 패널에서 찾아 바로 실행할 수 있어요.",
843    "category": "tool",
844    "kind": "upstream",
845    "author": "dasganni",
846    "license": "MIT",
847    "homepage": "https://github.com/agentic-workbench/agent-quick-menu",
848    "commands": [
849      "/menu"
850    ],
851    "requires": [],
852    "notes": [
853      "다른 플러그인의 명령을 대신 실행해 주는 구조라, 자기 플러그인이 아닌 명령은 두 번 눌러야(5초 내 재확인) 실행돼요.",
854      "Claude Code 2.1.287 이상, macOS/Linux 터미널이나 데스크톱 Code 탭 기준이에요. VS Code 채팅 패널은 지원 안 하고 Windows는 저자가 테스트하지 않았다고 README에 적혀 있어요."
855    ],
856    "permissions": [
857      "환경·설정 읽기",
858      "파일 읽기"
859    ]
860  },
861  {
862    "name": "image-view",
863    "displayName": "이미지 미리보기",
864    "summary": "붙여넣은 이미지를 `[Image #1]` 같은 글자 대신 입력창 위 썸네일로 보여줘요.",
865    "category": "tool",
866    "kind": "upstream",
867    "author": "gggodlin",
868    "license": "MIT",
869    "homepage": "https://github.com/GGGODLIN/cc-mod-image-view",
870    "commands": [],
871    "requires": [],
872    "notes": [],
873    "permissions": [
874      "프로그램 실행",
875      "대화 읽기",
876      "환경·설정 읽기",
877      "파일 읽기"
878    ]
879  },
880  {
881    "name": "now-playing",
882    "displayName": "나우 플레잉",
883    "summary": "macOS에서 Spotify로 재생 중인 곡과 가사를 입력창 위에 표시해요. 버튼이나 /music으로 재생·일시정지·이전·다음을 조작할 수 있어요.",
884    "category": "tool",
885    "kind": "upstream",
886    "author": "Hamza Zafar",
887    "license": "MIT",
888    "homepage": "https://github.com/hamzafer/claude-code-mods/tree/main/mods/now-playing",
889    "commands": [
890      "/music"
891    ],
892    "requires": [
893      "macOS",
894      "Spotify 데스크톱 앱"
895    ],
896    "notes": [
897      "macOS가 아니면 session.start에서 uname -s로 확인해 명령 등록·폴링을 전혀 하지 않고 조용히 꺼져 있어요(Linux/Windows에서 아무 영향 없음)."
898    ],
899    "permissions": [
900      "네트워크",
901      "프로그램 실행"
902    ]
903  },
904  {
905    "name": "paste-view",
906    "displayName": "붙여넣기 미리보기",
907    "summary": "붙여넣은 이미지와 긴 텍스트를 입력창 위에서 미리 확인할 수 있어요.",
908    "category": "tool",
909    "kind": "upstream",
910    "author": "Clément Décou",
911    "license": "MIT",
912    "homepage": "https://github.com/Amorfx/claude-paste-view",
913    "commands": [],
914    "requires": [],
915    "notes": [],
916    "permissions": [
917      "프로그램 실행",
918      "환경·설정 읽기",
919      "파일 읽기"
920    ]
921  },
922  {
923    "name": "pixel-player",
924    "displayName": "픽셀 뮤직 플레이어",
925    "summary": "mpv로 재생목록을 재생하면서 음악에 맞춰 반응하는 픽셀 아트 캐릭터를 패널에 표시해요.",
926    "category": "tool",
927    "kind": "upstream",
928    "author": "chrisluo5311",
929    "license": "MIT",
930    "homepage": "https://github.com/chrisluo5311/Pixel-Play",
931    "commands": [
932      "/music"
933    ],
934    "requires": [
935      "mpv (필수. 없으면 설치 안내만 뜨고 재생되지 않아요)",
936      "yt-dlp (유튜브 링크를 재생하려면 권장)"
937    ],
938    "notes": [
939      "재생목록(`~/.claude/pixel-play/playlist.txt`)에 유튜브 링크·직접 오디오 URL·로컬 파일 경로를 한 줄씩 추가해요. mpv가 그 주소로 직접 접속해 재생할 뿐, 모드 자체가 다른 곳에 접속하지는 않아요.",
940      "재생목록 파일은 플러그인 폴더가 아니라 `~/.claude/` 아래 저장돼 업데이트해도 유지돼요.",
941      "재생 제어(일시정지·재개)는 로컬 유닉스 소켓(IPC)으로만 이뤄지고 네트워크를 쓰지 않아요.",
942      "mpv 실행 인자 끝에 `--` 구분자가 없어서, 재생목록 줄이 `--`로 시작하면 mpv 옵션(예: `--script=`로 임의 Lua 스크립트 로드)으로 해석될 수 있어요. 재생목록은 본인이 직접 적는 로컬 파일이라 위험은 낮지만, 남이 준 재생목록 줄을 그대로 붙여넣지 않는 게 안전해요.",
943      "Windows는 지원하지 않고 macOS에서만 테스트됐어요."
944    ],
945    "permissions": [
946      "프로그램 실행",
947      "파일 쓰기",
948      "환경·설정 읽기",
949      "파일 읽기"
950    ]
951  },
952  {
953    "name": "prompt-rail",
954    "displayName": "프롬프트 레일",
955    "summary": "세션에서 보낸 프롬프트를 탐색 막대에 모아 표시해요. 마우스를 올리면 내용을 미리 보고 클릭하면 해당 지점으로 이동해요.",
956    "category": "tool",
957    "kind": "upstream",
958    "author": "oikon48",
959    "license": "MIT",
960    "homepage": "https://github.com/oikon48/prompt-rail",
961    "commands": [
962      "/prompt-rail"
963    ],
964    "requires": [],
965    "notes": [],
966    "permissions": [
967      "프로그램 실행",
968      "대화 읽기",
969      "파일 읽기"
970    ]
971  },
972  {
973    "name": "replay-theater",
974    "displayName": "Replay 극장",
975    "summary": "이번 턴에서 Claude가 고친 파일을 한 단계씩 diff로 다시 확인할 수 있어요.",
976    "category": "tool",
977    "kind": "upstream",
978    "author": "Claude Code DevRel",
979    "license": "Apache-2.0",
980    "homepage": "https://github.com/anthropics/claude-code-playground/tree/main/claude-code/mods/replay-theater",
981    "commands": [
982      "/replay"
983    ],
984    "requires": [],
985    "notes": [
986      "Write로 기존 파일을 덮어쓸 때는 그 파일의 원래 내용을 읽어와 diff를 만들어요(그 외 다른 파일은 읽지 않아요).",
987      "리플레이 기록은 메모리에만 있어서 세션을 새로고침하면 사라져요."
988    ],
989    "permissions": [
990      "도구 호출 제어",
991      "대화 읽기",
992      "파일 읽기"
993    ]
994  },
995  {
996    "name": "session-wrapped",
997    "displayName": "세션 랩드",
998    "summary": "`/wrapped`로 이번 세션의 도구 호출·테스트·비용 통계를 애니메이션으로 표시해요. 공유용 PNG 카드는 데스크톱에 저장해요.",
999    "category": "tool",
1000    "kind": "upstream",
1001    "author": "OneWave AI",
1002    "license": "MIT",
1003    "homepage": "https://github.com/OneWave-AI/claude-code-mods/tree/main/session-wrapped",
1004    "commands": [
1005      "/wrapped"
1006    ],
1007    "requires": [
1008      "python3"
1009    ],
1010    "notes": [
1011      "PNG 카드는 `~/Desktop`에 저장돼요.",
1012      "세션 종료 시 토스트 알림만 자동으로 뜨고, 실제 리캡 화면은 `/wrapped`를 직접 실행해야 열려요.",
1013      "통계는 전부 도구 호출 횟수·테스트 결과·토큰 사용량을 코드로 집계한 값이고, 요약 문구 생성에 모델을 쓰지 않아요."
1014    ],
1015    "permissions": [
1016      "프로그램 실행",
1017      "도구 호출 제어",
1018      "대화 읽기",
1019      "환경·설정 읽기"
1020    ]
1021  },
1022  {
1023    "name": "taxi-blackbox",
1024    "displayName": "택시 블랙박스",
1025    "summary": "도구 호출을 기록하고 오류나 거부 직전의 기록을 /blackbox에서 다시 확인할 수 있어요. 토큰·비밀번호는 가려서 기록해요.",
1026    "category": "tool",
1027    "kind": "upstream",
1028    "author": "개발동생 (devbrothers)",
1029    "license": "MIT",
1030    "homepage": "https://github.com/devbrother2024/devbrothers-mods",
1031    "commands": [
1032      "/blackbox"
1033    ],
1034    "requires": [],
1035    "notes": [],
1036    "permissions": [
1037      "도구 호출 제어",
1038      "대화 읽기"
1039    ]
1040  },
1041  {
1042    "name": "taxi-navi",
1043    "displayName": "택시 내비게이션",
1044    "summary": "할 일 목록에서 현재 진행 상태와 다음 할 일을 표시해요. 계획이 바뀌면 새 순서에 맞춰 목록을 갱신해요.",
1045    "category": "tool",
1046    "kind": "upstream",
1047    "author": "개발동생 (devbrothers)",
1048    "license": "MIT",
1049    "homepage": "https://github.com/devbrother2024/devbrothers-mods",
1050    "commands": [
1051      "/navi"
1052    ],
1053    "requires": [],
1054    "notes": [],
1055    "permissions": [
1056      "도구 호출 제어",
1057      "환경·설정 읽기",
1058      "소리"
1059    ]
1060  },
1061  {
1062    "name": "done-alarm",
1063    "displayName": "작업 끝 알림",
1064    "summary": "오래 걸린 작업이 끝나거나 확인이 필요하면 맥 알림·한국어 음성·폰 푸시(ntfy·텔레그램·슬랙)로 알려줘요",
1065    "category": "automation",
1066    "kind": "original",
1067    "author": "SeongGwangJu",
1068    "license": "MIT",
1069    "homepage": "https://github.com/SeongGwangJu/k-mods/tree/main/mods/done-alarm",
1070    "commands": [
1071      "/alarm"
1072    ],
1073    "requires": [],
1074    "notes": [],
1075    "permissions": [
1076      "네트워크",
1077      "프로그램 실행",
1078      "도구 호출 제어",
1079      "대화 읽기",
1080      "소리"
1081    ]
1082  },
1083  {
1084    "name": "agent-compact-advisor",
1085    "displayName": "컴팩트 타이밍 어드바이저",
1086    "summary": "지금이 /compact 하기 좋은 때인지 상태줄에 0~100점으로 표시해요. 압축할 때마다 목표·결정·남은 일을 포함한 보존 템플릿을 자동으로 추가해요.",
1087    "category": "automation",
1088    "kind": "upstream",
1089    "author": "apolenkov",
1090    "license": "MIT",
1091    "homepage": "https://github.com/apolenkov/agent-compact-advisor",
1092    "commands": [
1093      "/compact-advisor"
1094    ],
1095    "requires": [],
1096    "notes": [
1097      "P1(목표 달성 확률, 점수 비중 20)은 로컬 127.0.0.1:8010에 떠 있는 'Kev의 System One'이라는 별도 서비스가 있어야 값이 나와요. 없으면 조용히 빠지고 나머지 항목만으로 재계산돼요. 추가 설치 없이도 상태줄 점수·압축 보존 템플릿 같은 핵심 기능은 전부 동작해요.",
1098      "같은 저자의 다른 mod agent-shell-watch가 설치돼 있으면 백그라운드 작업(대기·실행 중)도 점수에 반영돼요. 없으면 그 부분이 '알 수 없음' 취급돼 점수가 60점에 캡돼요(정상 동작, 설치 필수 아님).",
1099      "리더오버 판정은 에이전트의 마지막 답변에서 정해진 접두어로 시작하는 줄만 봐요. 프롬프트에 '해야 할 일이 남아있다'는 식의 자연어 서술은 감지 못 해요(README에 명시된 한계)."
1100    ],
1101    "permissions": [
1102      "네트워크",
1103      "프로그램 실행",
1104      "도구 호출 제어",
1105      "대화 읽기"
1106    ]
1107  },
1108  {
1109    "name": "auto-handoff",
1110    "displayName": "오토 핸드오프",
1111    "summary": "컨텍스트가 한도에 도달하기 전에 Haiku가 요약한 브리핑을 만들고 새 대화로 전환해요. 필요하면 브리핑 페이지를 따로 열어 확인할 수 있어요.",
1112    "category": "automation",
1113    "kind": "upstream",
1114    "author": "Alex Hillman",
1115    "license": "MIT",
1116    "homepage": "https://github.com/alexknowshtml/claude-auto-handoff",
1117    "commands": [],
1118    "requires": [],
1119    "notes": [],
1120    "permissions": [
1121      "프로그램 실행",
1122      "파일 쓰기",
1123      "모델 호출",
1124      "도구 호출 제어",
1125      "프롬프트 입력",
1126      "대화 읽기",
1127      "환경·설정 읽기",
1128      "파일 읽기"
1129    ]
1130  },
1131  {
1132    "name": "ctx-handoff",
1133    "displayName": "컨텍스트 핸드오프",
1134    "summary": "컨텍스트가 한계에 가까워지면 새 대화용 핸드오프를 만들고 자동으로 전환해요. 사용자가 55분 동안 입력하지 않으면 프롬프트 캐시를 최대 3번 갱신해요.",
1135    "category": "automation",
1136    "kind": "upstream",
1137    "author": "cablate",
1138    "license": "MIT",
1139    "homepage": "https://github.com/cablate/ctx-handoff-mod",
1140    "commands": [
1141      "/handoff"
1142    ],
1143    "requires": [],
1144    "notes": [],
1145    "permissions": [
1146      "파일 쓰기",
1147      "모델 호출",
1148      "프롬프트 입력",
1149      "대화 읽기",
1150      "환경·설정 읽기",
1151      "파일 읽기"
1152    ]
1153  },
1154  {
1155    "name": "jev-skill-suggestion",
1156    "displayName": "스킬 자동 추천",
1157    "summary": "프롬프트마다 맞는 스킬 하나를 판단해 자동으로 불러오고, 스킬 목록은 컨텍스트에 넣지 않아요. API 키가 있으면 TypeSafe Jev로, 없으면 Claude 분류기로 판단해요",
1158    "category": "automation",
1159    "kind": "upstream",
1160    "author": "claude-code-templates",
1161    "license": "MIT",
1162    "homepage": "https://github.com/davila7/claude-code-templates/tree/375af9018a40e330e81542f59054daaa088c21aa/cli-tool/components/mods/productivity/jev-skill-suggestion",
1163    "commands": [
1164      "/jev-skill-suggestion:setup"
1165    ],
1166    "requires": [],
1167    "notes": [
1168      "API 키(typesafeApiKey 또는 gatewayApiKey)를 넣으면 프롬프트 전문과 후보 스킬 이름·설명, 선별된 스킬 SKILL.md 앞부분이 TypeSafe 서버로 전송돼요(저자가 코드 주석·README에 직접 명시한 사실)",
1169      "키가 없으면 Claude Code 자체 모델 분류기($.model.classify)를 프롬프트마다 호출해 사용자 Claude 사용량을 소모해요",
1170      "모든 실패 경로가 fail-open이에요. 요청 실패나 타임아웃(기본 800ms)이면 제안 없이 프롬프트를 그대로 통과시켜요",
1171      "/jev-skill-suggestion:setup은 mod가 직접 설정 파일을 쓰지 않고, Claude에게 변경 계획을 프롬프트로 건네 Claude의 Edit 도구로 사용자 확인을 거쳐 수정하게 해요"
1172    ],
1173    "permissions": [
1174      "네트워크",
1175      "모델 호출",
1176      "대화 읽기",
1177      "환경·설정 읽기",
1178      "파일 읽기"
1179    ]
1180  },
1181  {
1182    "name": "next-steps",
1183    "displayName": "다음 할 일 제안",
1184    "summary": "턴이 끝날 때마다 다음에 보낼 법한 프롬프트 2~3개를 입력창 위에 제안해요. 숫자 키를 누르면 선택한 문구가 입력창에 입력돼요.",
1185    "category": "automation",
1186    "kind": "upstream",
1187    "author": "Hamza Zafar",
1188    "license": "MIT",
1189    "homepage": "https://github.com/hamzafer/claude-code-mods/tree/main/mods/next-steps",
1190    "commands": [],
1191    "requires": [],
1192    "notes": [
1193      "같은 저장소의 where-am-i와 서로 인지하도록 짜여 있어요. next-steps 목록이 떠 있는 동안 where-am-i는 자기 '다음 할 일' 칸을 비워서 같은 내용이 중복 표시되지 않게 해요."
1194    ],
1195    "permissions": [
1196      "모델 호출",
1197      "대화 읽기"
1198    ]
1199  },
1200  {
hooks/lib.ts 261 lines
1// $를 받지 않는 순수 함수만 모아 둔 파일. register.ts가 이 함수들을 가져다 쓴다.
2// 테스트하기 쉽도록 $, UI 엘리먼트, 부수효과를 전혀 참조하지 않는다.
3
4import { MARKETPLACE, PERMISSION_ORDER, type CatalogEntry, type CategoryId } from './catalog'
5
6// ---------------------------------------------------------------------------
7// 카탈로그: 필터·정렬·숨김
8// ---------------------------------------------------------------------------
9
10/** 상점 자신(selfName)을 목록에서 뺀 카탈로그를 돌려준다. */
11export function visibleCatalog(
12  catalog: readonly CatalogEntry[],
13  selfName: string,
14): CatalogEntry[] {
15  return catalog.filter(entry => entry.name !== selfName)
16}
17
18/** 탭(카테고리)으로 거른다. 'all'이면 전체를 그대로 돌려준다. */
19export function filterByTab(
20  catalog: readonly CatalogEntry[],
21  tab: CategoryId | 'all',
22): CatalogEntry[] {
23  return tab === 'all' ? [...catalog] : catalog.filter(entry => entry.category === tab)
24}
25
26/** featured 항목을 앞으로 보낸다. 나머지는 원래 순서를 지킨다(안정 정렬). */
27export function sortFeaturedFirst(catalog: readonly CatalogEntry[]): CatalogEntry[] {
28  return [...catalog].sort((a, b) => Number(b.featured === true) - Number(a.featured === true))
29}
30
31/** 탭 버튼에 보일 개수. 'all' 키에 전체 개수가 들어간다. */
32export function categoryCounts(
33  catalog: readonly CatalogEntry[],
34): Record<'all' | CategoryId, number> {
35  const counts = { all: catalog.length } as Record<'all' | CategoryId, number>
36  for (const entry of catalog) {
37    counts[entry.category] = (counts[entry.category] ?? 0) + 1
38  }
39  return counts
40}
41
42// ---------------------------------------------------------------------------
43// 폭 3단계
44// ---------------------------------------------------------------------------
45
46export type WidthTier = 'wide' | 'normal' | 'narrow'
47
48/** 넓음(>=100) · 보통(70~99) · 좁음(<70). CONTRIBUTING.md 화면 원칙 3번. */
49export function widthTierOf(columns: number): WidthTier {
50  if (columns >= 100) return 'wide'
51  if (columns >= 70) return 'normal'
52  return 'narrow'
53}
54
55// ---------------------------------------------------------------------------
56// 권한 칩: 가장 민감한 권한을 고른다
57// ---------------------------------------------------------------------------
58
59// 민감도 높은 순. 네트워크 > 명령 실행 > 파일 쓰기 > 대화 읽기 > ... > 화면만(가장 약함).
60// 민감한 것부터. scripts/build.mjs가 catalog.ts에 생성하는 순서를 그대로 쓴다
61export const PERMISSION_PRIORITY: readonly string[] = PERMISSION_ORDER
62
63function permissionRank(permission: string): number {
64  const index = PERMISSION_PRIORITY.indexOf(permission)
65  return index === -1 ? PERMISSION_PRIORITY.length : index
66}
67
68/** 목록 중 가장 민감한 권한 라벨 하나를 돌려준다. 비어 있으면 '화면만'. */
69export function mostSensitivePermission(permissions: readonly string[]): string {
70  if (permissions.length === 0) return '화면만'
71  return permissions.reduce((best, current) =>
72    permissionRank(current) < permissionRank(best) ? current : best,
73  )
74}
75
76export type PermissionDisplay = {
77  primary: string
78  extra: string[]
79}
80
81/**
82 * 폭 단계에 맞춰 보여줄 권한을 고른다. 좁은 화면에서는 가장 민감한 것 하나만,
83 * 넓은 화면에서는 전부, 보통 화면에서는 최대 2개까지 추가로 보여준다.
84 */
85export function permissionDisplayOf(
86  permissions: readonly string[],
87  tier: WidthTier,
88): PermissionDisplay {
89  if (permissions.length === 0) return { primary: '화면만', extra: [] }
90  const primary = mostSensitivePermission(permissions)
91  if (tier === 'narrow') return { primary, extra: [] }
92  const rest = permissions.filter(permission => permission !== primary)
93  const maxExtra = tier === 'wide' ? rest.length : Math.min(rest.length, 2)
94  return { primary, extra: rest.slice(0, maxExtra) }
95}
96
97// ---------------------------------------------------------------------------
98// kind 칩 라벨·색
99// ---------------------------------------------------------------------------
100
101export type ChipColor = 'claude' | 'warning' | 'subtle' | 'success' | 'permission' | 'error' | 'suggestion'
102
103export const KIND_LABEL: Record<CatalogEntry['kind'], string> = {
104  upstream: '원본',
105  patched: '수정판',
106  original: '오리지널',
107  bundle: '묶음',
108}
109
110export const KIND_COLOR: Record<CatalogEntry['kind'], ChipColor> = {
111  upstream: 'subtle',
112  patched: 'warning',
113  original: 'claude',
114  bundle: 'success',
115}
116
117// ---------------------------------------------------------------------------
118// claude 버전 비교
119// ---------------------------------------------------------------------------
120
121export const MIN_CLAUDE_VERSION = '2.1.287'
122
123/** "2.1.291" 같은 점 구분 숫자 버전을 비교 가능한 튜플로 바꾼다. */
124export function parseVersionTuple(version: string): number[] {
125  return version
126    .trim()
127    .split('.')
128    .map(part => {
129      const n = Number.parseInt(part, 10)
130      return Number.isFinite(n) ? n : 0
131    })
132}
133
134/** version이 min 이상이면 true. 자리 수가 달라도(2.1 vs 2.1.0) 부족한 자리는 0으로 본다. */
135export function isVersionAtLeast(version: string, min: string): boolean {
136  const v = parseVersionTuple(version)
137  const m = parseVersionTuple(min)
138  const length = Math.max(v.length, m.length)
139  for (let i = 0; i < length; i++) {
140    const a = v[i] ?? 0
141    const b = m[i] ?? 0
142    if (a !== b) return a > b
143  }
144  return true
145}
146
147/** "2.1.291 (Claude Code)" 같은 출력에서 첫 x.y.z 버전 토큰을 뽑는다. */
148export function firstVersionToken(text: string): string | null {
149  const match = /(\d+\.\d+\.\d+)/.exec(text)
150  return match?.[1] ?? null
151}
152
153// ---------------------------------------------------------------------------
154// 설치 명령 조립
155// ---------------------------------------------------------------------------
156
157export function installArgsFor(name: string): string[] {
158  return ['claude', 'plugin', 'install', `${name}@${MARKETPLACE}`, '--scope', 'user']
159}
160
161export function uninstallArgsFor(name: string): string[] {
162  return ['claude', 'plugin', 'uninstall', `${name}@${MARKETPLACE}`, '--scope', 'user']
163}
164
165export function commandLineFor(argv: readonly string[]): string {
166  return argv.join(' ')
167}
168
169export const MARKETPLACE_ADD_COMMAND = `/plugin marketplace add SeongGwangJu/${MARKETPLACE}`
170
171// ---------------------------------------------------------------------------
172// $.process.run 결과 해석
173// ---------------------------------------------------------------------------
174
175export type ProcessResult = {
176  exitCode: number
177  stdout: string
178  stderr: string
179}
180
181export type InstallOutcome =
182  | { kind: 'installed'; message: string }
183  | { kind: 'already'; message: string }
184  | { kind: 'marketplace-missing'; message: string }
185  | { kind: 'failed'; message: string }
186
187function isMarketplaceMissing(text: string): boolean {
188  return /marketplace/i.test(text) && /not found/i.test(text)
189}
190
191export function parseInstallOutcome(result: ProcessResult): InstallOutcome {
192  const text = `${result.stdout}\n${result.stderr}`.trim()
193  if (isMarketplaceMissing(text)) return { kind: 'marketplace-missing', message: text }
194  if (result.exitCode === 0) {
195    return /already installed/i.test(text)
196      ? { kind: 'already', message: text }
197      : { kind: 'installed', message: text }
198  }
199  return { kind: 'failed', message: text || `종료 코드 ${result.exitCode}` }
200}
201
202export type UninstallOutcome =
203  | { kind: 'uninstalled'; message: string }
204  | { kind: 'failed'; message: string }
205
206export function parseUninstallOutcome(result: ProcessResult): UninstallOutcome {
207  const text = `${result.stdout}\n${result.stderr}`.trim()
208  if (result.exitCode === 0) return { kind: 'uninstalled', message: text }
209  return { kind: 'failed', message: text || `종료 코드 ${result.exitCode}` }
210}
211
212// ---------------------------------------------------------------------------
213// 설치 상태 읽기
214// ---------------------------------------------------------------------------
215
216/**
217 * $.settings.read()가 돌려준 값에서 enabledPlugins를 읽어, 카탈로그 중
218 * "<name>@k-mods": true 로 적힌 이름만 설치됨으로 본다.
219 */
220export function installedNamesFrom(
221  settings: Record<string, unknown>,
222  catalog: readonly CatalogEntry[],
223): Set<string> {
224  const result = new Set<string>()
225  const enabled = settings.enabledPlugins
226  if (enabled === null || typeof enabled !== 'object') return result
227  const map = enabled as Record<string, unknown>
228  for (const entry of catalog) {
229    if (map[`${entry.name}@${MARKETPLACE}`] === true) result.add(entry.name)
230  }
231  return result
232}
233
234// ---------------------------------------------------------------------------
235// 배너(오류 안내)
236// ---------------------------------------------------------------------------
237
238export type Banner =
239  | { kind: 'none' }
240  | { kind: 'no-marketplace'; addCommand: string }
241  | { kind: 'error'; text: string; copyText: string }
242
243export function errorBannerOf(argv: readonly string[], message: string): Banner {
244  const short = message.length > 160 ? `${message.slice(0, 160)}…` : message
245  return {
246    kind: 'error',
247    text: short === '' ? '명령이 실패했어요.' : `명령이 실패했어요: ${short}`,
248    copyText: commandLineFor(argv),
249  }
250}
251
252export function messageOf(error: unknown): string {
253  if (error instanceof Error) return error.message
254  if (typeof error === 'string') return error
255  try {
256    return JSON.stringify(error)
257  } catch {
258    return String(error)
259  }
260}
261
hooks/view.ts 286 lines
1// 패널 화면을 순수하게 조립하는 함수들. $를 전혀 참조하지 않는다 — register.ts가
2// $.ui.resolve(e)로 얻은 엘리먼트(kit)와 모델, 콜백(actions)만 건네준다.
3
4import type {
5  BoxProps,
6  ButtonProps,
7  ElementConstructor,
8  LinkProps,
9  RenderElement,
10  RenderSurface,
11  TextProps,
12} from 'claude-code'
13
14import { CATEGORIES, type CatalogEntry, type CategoryId } from './catalog'
15import {
16  type Banner,
17  KIND_COLOR,
18  KIND_LABEL,
19  categoryCounts,
20  filterByTab,
21  permissionDisplayOf,
22  sortFeaturedFirst,
23  type WidthTier,
24} from './lib'
25
26export type Kit = {
27  Box: ElementConstructor<BoxProps>
28  Text: ElementConstructor<TextProps>
29  Button: ElementConstructor<ButtonProps>
30  Link: ElementConstructor<LinkProps>
31}
32
33export type StoreModel = {
34  tab: CategoryId | 'all'
35  expanded: string | null
36  installed: ReadonlySet<string>
37  busy: ReadonlySet<string>
38  banner: Banner
39  justInstalled: string | null
40  tier: WidthTier
41}
42
43export type StoreActions = {
44  setTab: (tab: CategoryId | 'all') => void
45  toggleDetail: (name: string) => void
46  install: (name: string) => void
47  uninstall: (name: string) => void
48  applyReload: () => void
49  copyCommand: (text: string, surface: RenderSurface) => void
50  dismissBanner: () => void
51}
52
53/** 상점 패널 전체를 그린다. catalog는 이미 자기 자신(mod-store)을 뺀 목록이어야 한다. */
54export function paneView(
55  kit: Kit,
56  catalog: readonly CatalogEntry[],
57  model: StoreModel,
58  actions: StoreActions,
59): RenderElement {
60  const { Box, Text } = kit
61  const counts = categoryCounts(catalog)
62  const shown = filterByTab(sortFeaturedFirst(catalog), model.tab)
63
64  return Box({
65    flexDirection: 'column',
66    children: [
67      Text({ bold: true, children: ['k-mods 모드 상점'] }),
68      ...bannerRows(kit, model, actions),
69      ...justInstalledRows(kit, model, actions),
70      Text({ children: [' '] }),
71      tabsRow(kit, model, actions, counts),
72      Text({ children: [' '] }),
73      ...(shown.length === 0
74        ? [Text({ dimColor: true, children: ['이 카테고리에는 아직 항목이 없어요.'] })]
75        : shown.flatMap(entry => entryRows(kit, entry, model, actions))),
76      Text({ dimColor: true, children: ['Tab 이동 · Enter 누르기 · Esc 닫기'] }),
77    ],
78  })
79}
80
81function bannerRows(kit: Kit, model: StoreModel, actions: StoreActions): RenderElement[] {
82  const { Box, Text, Button } = kit
83  if (model.banner.kind === 'none') return []
84
85  // 세로로 쌓는다: row로 두면 좁은 화면에서 Text마다 따로 줄바꿈되어 글자가 뒤섞인다
86  // (실제 세션에서 확인한 문제 — 한 줄짜리 문장이 Box 폭에 맞춰 조각나 보였다).
87  if (model.banner.kind === 'no-marketplace') {
88    const banner = model.banner
89    return [
90      Box({
91        flexDirection: 'column',
92        children: [
93          Text({ color: 'warning', children: ['마켓플레이스가 아직 없어요.'] }),
94          Box({
95            flexDirection: 'row',
96            columnGap: 1,
97            children: [
98              Text({ dimColor: true, children: [banner.addCommand] }),
99              Button({
100                key: 'banner-copy',
101                label: '복사',
102                plain: true,
103                onPress: press => actions.copyCommand(banner.addCommand, press.surface),
104              }),
105            ],
106          }),
107        ],
108      }),
109    ]
110  }
111
112  const banner = model.banner
113  return [
114    Box({
115      flexDirection: 'column',
116      children: [
117        Text({ color: 'error', children: [banner.text] }),
118        Box({
119          flexDirection: 'row',
120          columnGap: 2,
121          children: [
122            Button({
123              key: 'banner-copy',
124              label: '복사',
125              plain: true,
126              onPress: press => actions.copyCommand(banner.copyText, press.surface),
127            }),
128            Button({ key: 'banner-dismiss', label: '닫기', plain: true, onPress: () => actions.dismissBanner() }),
129          ],
130        }),
131      ],
132    }),
133  ]
134}
135
136function justInstalledRows(kit: Kit, model: StoreModel, actions: StoreActions): RenderElement[] {
137  if (model.justInstalled === null) return []
138  const { Box, Text, Button } = kit
139  return [
140    Box({
141      flexDirection: 'row',
142      columnGap: 1,
143      children: [
144        Text({ color: 'success', children: ['설치했어요.'] }),
145        Button({ key: 'apply-reload', label: '지금 적용', onPress: () => actions.applyReload() }),
146      ],
147    }),
148  ]
149}
150
151function tabsRow(
152  kit: Kit,
153  model: StoreModel,
154  actions: StoreActions,
155  counts: Record<'all' | CategoryId, number>,
156): RenderElement {
157  const { Box, Button } = kit
158  const tabs: { id: CategoryId | 'all'; label: string; hotkey: string }[] = [
159    { id: 'all', label: '전체', hotkey: '0' },
160    ...CATEGORIES.map((category, index) => ({
161      id: category.id,
162      label: category.label,
163      hotkey: String(index + 1),
164    })),
165  ]
166
167  return Box({
168    flexDirection: 'row',
169    flexWrap: 'wrap',
170    columnGap: 2,
171    children: tabs.map(item =>
172      Button({
173        // plain + hotkey가 이미 "숫자: 라벨" 형태로 그려 주므로 라벨에 다시 적지 않는다
174        key: `tab-${item.id}`,
175        label: `${item.label} (${counts[item.id] ?? 0})`,
176        hotkey: item.hotkey,
177        plain: true,
178        dimColor: model.tab !== item.id,
179        onPress: () => actions.setTab(item.id),
180      }),
181    ),
182  })
183}
184
185function entryRows(
186  kit: Kit,
187  entry: CatalogEntry,
188  model: StoreModel,
189  actions: StoreActions,
190): RenderElement[] {
191  const { Box, Text, Button } = kit
192  const isInstalled = model.installed.has(entry.name)
193  const isBusy = model.busy.has(entry.name)
194  const isExpanded = model.expanded === entry.name
195  const permissions = permissionDisplayOf(entry.permissions, model.tier)
196
197  const nameLine = Box({
198    flexDirection: 'row',
199    columnGap: 1,
200    children: [
201      Text({
202        backgroundColor: KIND_COLOR[entry.kind],
203        color: 'inverseText',
204        children: [` ${KIND_LABEL[entry.kind]} `],
205      }),
206      Text({ bold: true, children: [entry.displayName] }),
207      ...(isInstalled
208        ? [Text({ backgroundColor: 'success', color: 'inverseText', children: [' 설치됨 '] })]
209        : []),
210    ],
211  })
212
213  const summaryLine = Text({ dimColor: true, children: [entry.summary] })
214
215  const permissionTexts: RenderElement[] = [Text({ color: 'permission', children: [permissions.primary] })]
216  if (permissions.extra.length > 0) {
217    permissionTexts.push(Text({ dimColor: true, children: [' · ' + permissions.extra.join(' · ')] }))
218  }
219
220  const installLabel = isBusy
221    ? isInstalled
222      ? '제거 중…'
223      : '설치 중…'
224    : isInstalled
225      ? '설치됨 · 제거'
226      : '설치'
227
228  const installButton = Button({
229    key: `install-${entry.name}`,
230    label: installLabel,
231    dimColor: isBusy,
232    onPress: () => {
233      if (isBusy) return
234      if (isInstalled) actions.uninstall(entry.name)
235      else actions.install(entry.name)
236    },
237  })
238
239  const detailButton = Button({
240    key: `detail-${entry.name}`,
241    label: isExpanded ? '접기' : '자세히',
242    plain: true,
243    onPress: () => actions.toggleDetail(entry.name),
244  })
245
246  const actionLines: RenderElement[] =
247    model.tier === 'narrow'
248      ? [
249          Box({ flexDirection: 'row', children: permissionTexts }),
250          Box({ flexDirection: 'row', columnGap: 2, children: [installButton, detailButton] }),
251        ]
252      : [
253          Box({
254            flexDirection: 'row',
255            columnGap: 2,
256            children: [...permissionTexts, installButton, detailButton],
257          }),
258        ]
259
260  const rows: RenderElement[] = [nameLine]
261  if (model.tier !== 'narrow') rows.push(summaryLine)
262  rows.push(...actionLines)
263  if (isExpanded) rows.push(detailBlock(kit, entry))
264  rows.push(Text({ children: [' '] }))
265
266  return rows
267}
268
269function detailBlock(kit: Kit, entry: CatalogEntry): RenderElement {
270  const { Box, Text, Link } = kit
271  const commands = entry.commands.length > 0 ? entry.commands.join(', ') : '없음'
272  const requires = entry.requires.length > 0 ? entry.requires.join(', ') : '없음'
273
274  return Box({
275    flexDirection: 'column',
276    paddingLeft: 2,
277    children: [
278      Text({ dimColor: true, children: [`제작자 ${entry.author} · 라이선스 ${entry.license}`] }),
279      Text({ dimColor: true, children: [`명령 ${commands}`] }),
280      Text({ dimColor: true, children: [`준비물 ${requires}`] }),
281      ...entry.notes.map(note => Text({ dimColor: true, children: [`· ${note}`] })),
282      ...(entry.homepage !== undefined ? [Link({ href: entry.homepage, label: '홈페이지' })] : []),
283    ],
284  })
285}
286