턴이 끝나면 다음에 할 일을 최대 4개 제안하고, 체크박스로 골라 입력창에 채운다

새 백엔드 프로젝트를 시작할 때 가져다 쓰는 Claude Code 기본 세팅입니다.
에이전트, 스킬, 커맨드를 이 저장소에서 만들고 검증한 뒤, install.sh로 전역(~/.claude)이나 프로젝트(.claude/)에 심링크를 걸어 씁니다. 다루는 범위는 백엔드(REST API, 서비스, 영속성)이고, 특정 프로젝트나 스택에 묶이지 않게 작성합니다.
my-harness/
├── agents/ # 서브에이전트
├── skills/ # 스택별 구현 패턴
├── commands/ # 슬래시 커맨드
├── rules/ # 규칙 (경로별 지침)
├── hooks/ # 훅 (폴더 하나 = 훅 하나, install.sh가 settings에 병합)
├── mods/ # Claude Code 모드 (폴더 하나 = 모드 하나, install.sh가 전역 settings에 등록)
├── plugins/ # 외부 플러그인 목록 (파일 하나 = 플러그인 하나, install.sh가 claude plugin install로 설치)
├── prompts/ # 복사해 쓰는 프롬프트 (설치 대상 아님)
├── docs/ # 플러그인 사용법, guide/ AI 사용 가이드 (설치 대상 아님)
├── playground/ # 방식별 비교 실험 (설치 대상 아님)
├── install.sh
└── CLAUDE.md
기능 개발 — feature-pm이 나머지 넷을 순서대로 부립니다.
| 이름 | 하는 일 |
|---|---|
feature-pm | 요구사항 분해, 스택 확정, 워커 조율 (메인 세션에서 직접 호출) |
backend-designer | API와 DB 스키마를 함께 설계 |
backend-impl | 설계를 코드로 구현 |
boundary-verifier | 설계와 구현이 서로 맞는지 검증 |
test-writer | 단위·통합 테스트 작성 |
진단 — 코드를 직접 고치지 않고 원인과 개선안만 알려 줍니다.
| 이름 | 하는 일 |
|---|---|
architecture-expert | 구조·의존성 진단, 기술 선택 비교 |
code-reviewer | 가독성, 네이밍, 에러 처리, 중복 리뷰 |
debugger | 버그 재현, 근본 원인 특정 |
performance-optimizer | 병목, N+1, 복잡도 진단 |
security-auditor | 인증·인가, 인젝션 등 보안 점검 |
| 이름 | 하는 일 |
|---|---|
spring-boot | Spring Boot 3.x 구현 패턴 |
nestjs | NestJS 구현 패턴 |
screen-spec-analyzer | 화면 캡처나 화면정의서에서 API 응답 필드 추출 |
| 이름 | 하는 일 |
|---|---|
/commit | 변경을 논리 단위로 나눠 커밋 |
/push | 보호 브랜치와 강제 푸시를 확인하고 푸시 |
/pre-pr | PR 올리기 전 코드 리뷰 (테스트·린트·진단 후 BLOCK/READY 판정) |
/post-pr | PR의 CI 결과와 리뷰 코멘트 대응, 머지 후 브랜치 정리 |
| 이름 | 하는 일 |
|---|---|
next-steps | 턴이 끝나면 다음 할 일을 최대 4개 제안, 체크박스로 골라 입력창에 채움 (mods/README.md) |
context-bar | 입력창 위에 컨텍스트 창 사용량을 카테고리별 색 막대와 비율로 표시 (mods/README.md) |
agent-monitor | 서브에이전트의 상태·경과 시간·도구 호출 수·토큰을 사이드 패널에 실시간 표시 (mods/README.md) |
프롬프트를 복사해 {빈칸}만 채워 씁니다.
| 상황 | 프롬프트 |
|---|---|
| 기능 개발 (설계 → 구현 → 테스트) | prompts/feature-development.md |
| 커밋·배포 전 코드 리뷰 | prompts/code-review.md |
| 화면 캡처로 API 응답 설계 | prompts/screen-spec.md |
{증상 — 무엇이 언제부터 어떻게}. 순서대로 진행해줘.
1) debugger로 재현 경로를 잡고 문제 구간을 file:line으로 특정
2) performance-optimizer로 {관찰 지점}을 포함해 병목을 우선순위로 진단
3) architecture-expert 관점에서 구조적 개선안을 트레이드오프로 비교
셋 다 진단만 하고 코드는 고치지 마. 마지막에 안 A/B/C로 정리해줘.
./install.sh # 대화형: 대상·유형·항목을 골라 설치
./install.sh list # 설치 상태 확인
./install.sh uninstall # 우리 심링크만 제거
스크립트에서 쓰거나 세부 옵션(--type, --project, --force, --dry-run 등)이 필요하면 ./install.sh -h를 본다.
hooks/register.tsx 289 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, ModelForkResult, Register, RenderElement } from 'claude-code'
3
4import type { Mode, Phase, UiSettings, Usage } from '../types'
5import {
6 addUsage,
7 applyUi,
8 askFor,
9 compactUsage,
10 composePrompt,
11 DEFAULT_LIMIT,
12 DEFAULT_UI,
13 formatTokens,
14 formatUi,
15 formatUsage,
16 NO_USAGE,
17 normalizeUi,
18 parseSteps,
19 recentTranscript,
20 toggle,
21} from './steps'
22
23const phase = atom({ plugin: 'next-steps', key: 'phase' } as const, 'idle' as Phase)
24const steps = atom({ plugin: 'next-steps', key: 'steps' } as const, [] as string[])
25const picked = atom({ plugin: 'next-steps', key: 'picked' } as const, [] as number[])
26// 제안을 요청한 턴. 새 프롬프트가 들어오면 비워서 늦게 도착한 제안을 버린다.
27const turnId = atom({ plugin: 'next-steps', key: 'turnId' } as const, '')
28const isOn = atom({ plugin: 'next-steps', key: 'isOn' } as const, true)
29const mode = atom({ plugin: 'next-steps', key: 'mode' } as const, 'haiku' as Mode)
30const limit = atom({ plugin: 'next-steps', key: 'limit' } as const, DEFAULT_LIMIT)
31// 자동 제안을 건너뛰었을 때의 컨텍스트 크기. 밴드 안내에 쓴다.
32const contextTokens = atom({ plugin: 'next-steps', key: 'contextTokens' } as const, 0)
33// 제안 생성 호출의 토큰 사용량. 이 세션 동안만 모은다.
34const lastUsage = atom({ plugin: 'next-steps', key: 'lastUsage' } as const, null as Usage | null)
35const totalUsage = atom({ plugin: 'next-steps', key: 'totalUsage' } as const, NO_USAGE)
36const calls = atom({ plugin: 'next-steps', key: 'calls' } as const, 0)
37const ui = atom({ plugin: 'next-steps', key: 'ui' } as const, DEFAULT_UI as UiSettings)
38
39const STORE_IS_ON = 'isOn'
40const STORE_MODE = 'mode'
41const STORE_LIMIT = 'limit'
42const STORE_UI = 'ui'
43const HELP = '사용법: /next-steps [on|off|usage|model haiku|fork|limit <토큰, 0은 끔>|ui [count|bold|gap|tokens <값>]]'
44
45async function clear($: EngineInterface) {
46 await update($, turnId, () => '')
47 await update($, phase, () => 'idle' as Phase)
48 await update($, steps, () => [])
49 await update($, picked, () => [])
50}
51
52async function ask($: EngineInterface): Promise<ModelForkResult> {
53 const prompt = askFor((await read($, ui)).count)
54 if ((await read($, mode)) === 'fork') return $.model.fork({ prompt })
55
56 const transcript = recentTranscript(await $.session.messages())
57 if (transcript === '') return { isAnswered: false, reason: 'nothing-to-fork' }
58 return $.model.complete({
59 model: 'haiku',
60 prompt: `<대화>\n${transcript}\n</대화>\n\n${prompt}`,
61 maxTokens: 500,
62 })
63}
64
65async function suggest($: EngineInterface, id: string) {
66 await update($, turnId, () => id)
67 await update($, steps, () => [])
68 await update($, picked, () => [])
69 await update($, phase, () => 'loading' as Phase)
70
71 const reply = await ask($)
72 // 버려진 호출도 비용은 들었으니 누적에는 넣는다.
73 if ('usage' in reply) {
74 const usage = reply.usage
75 await update($, totalUsage, total => addUsage(total, usage))
76 await update($, calls, n => n + 1)
77 }
78 if ((await read($, turnId)) !== id) return
79
80 // 밴드에 보이는 토큰은 지금 보이는 목록을 만든 호출의 것이어야 한다.
81 if ('usage' in reply) {
82 const usage = reply.usage
83 await update($, lastUsage, () => usage)
84 }
85 const found = reply.isAnswered ? parseSteps(reply.text, (await read($, ui)).count) : []
86 await update($, steps, () => found)
87 await update($, phase, () => (found.length > 0 ? 'ready' : 'idle') as Phase)
88}
89
90// fork는 대화 전체를 보내므로, 대화가 기준보다 길면 자동 제안은 건너뛴다.
91async function isTooLong($: EngineInterface): Promise<boolean> {
92 const max = await read($, limit)
93 if ((await read($, mode)) !== 'fork' || max <= 0) return false
94
95 const tokens = (await $.session.usage()).context.tokens ?? 0
96 if (tokens <= max) return false
97
98 await clear($)
99 await update($, contextTokens, () => tokens)
100 await update($, phase, () => 'skipped' as Phase)
101 return true
102}
103
104async function fill($: EngineInterface) {
105 const list = await read($, steps)
106 const chosen = (await read($, picked)).map(i => list[i]).filter((s): s is string => !!s)
107 if (chosen.length === 0) {
108 $.ui.toast('먼저 번호로 항목을 골라 주세요')
109 return
110 }
111
112 const filled = await $.prompt.fill({ text: composePrompt(chosen), mode: 'insert' })
113 if (filled.isFilled) await clear($)
114 else $.ui.toast('입력창에 넣지 못했어요')
115}
116
117export const register: Register = on => {
118 on('session.start', async ($, e, next) => {
119 const storedIsOn = await $.store.get(STORE_IS_ON)
120 const storedMode = await $.store.get(STORE_MODE)
121 const storedLimit = await $.store.get(STORE_LIMIT)
122 await update($, isOn, () => storedIsOn !== false)
123 await update($, mode, () => (storedMode === 'fork' ? 'fork' : 'haiku') as Mode)
124 await update($, limit, () => (typeof storedLimit === 'number' ? storedLimit : DEFAULT_LIMIT))
125 const storedUi = normalizeUi(await $.store.get(STORE_UI))
126 await update($, ui, () => storedUi)
127 await $.command.register({
128 name: 'next-steps',
129 description: '다음 스텝 제안: 인자 없으면 지금 제안, on/off, usage, model haiku|fork, limit <토큰>, ui <항목> <값>',
130 argumentHint: '[on|off|usage|model|limit|ui]',
131 })
132
133 // 모드가 다시 로드되면 진행 중이던 호출은 끊기지만 $.state는 남는다.
134 // "정리 중"인 채로 남은 턴이 있으면 이어서 다시 요청한다.
135 const pending = await read($, turnId)
136 if (pending !== '' && (await read($, phase)) === 'loading') {
137 void suggest($, pending)
138 }
139
140 return next(e)
141 })
142
143 on('command.run', { command: 'next-steps' }, async ($, e) => {
144 const [sub = '', value = ''] = e.args.trim().toLowerCase().split(/\s+/)
145
146 if (sub === 'on' || sub === 'off') {
147 const isOnNow = sub === 'on'
148 await update($, isOn, () => isOnNow)
149 await $.store.set(STORE_IS_ON, isOnNow)
150 if (!isOnNow) await clear($)
151 return { text: `다음 스텝 자동 제안을 ${isOnNow ? '켰어요' : '껐어요'}.` }
152 }
153 if (sub === 'model') {
154 if (value !== 'haiku' && value !== 'fork') return { text: HELP }
155 await update($, mode, () => value as Mode)
156 await $.store.set(STORE_MODE, value)
157 return {
158 text: value === 'haiku'
159 ? '다음 스텝을 Haiku로 만들어요. 최근 대화만 보내서 비용이 낮고 일정해요.'
160 : '다음 스텝을 세션 모델로 만들어요. 대화 전체를 보고 만들어서 정확하지만, 대화가 길수록 비용이 늘어요.',
161 }
162 }
163 if (sub === 'limit') {
164 const max = Number(value)
165 if (value === '' || !Number.isInteger(max) || max < 0) return { text: HELP }
166 await update($, limit, () => max)
167 await $.store.set(STORE_LIMIT, max)
168 return {
169 text: max === 0
170 ? '대화 길이와 상관없이 자동 제안해요.'
171 : `fork 모드에서 컨텍스트가 ${formatTokens(max)} 토큰을 넘으면 자동 제안을 건너뛰어요.`,
172 }
173 }
174 if (sub === 'ui') {
175 const [, key = '', value = ''] = e.args.trim().toLowerCase().split(/\s+/)
176 const current = await read($, ui)
177 if (key === '') return { text: `다음 스텝 화면: ${formatUi(current)}\n바꾸기: /next-steps ui count 1~9 · bold on|off · gap 0~2 · tokens on|off` }
178 const changed = applyUi(current, key, value)
179 if (typeof changed === 'string') return { text: changed }
180 await update($, ui, () => changed)
181 await $.store.set(STORE_UI, changed)
182 return { text: `다음 스텝 화면: ${formatUi(changed)}${key === 'count' ? ' (항목 수는 다음 제안부터)' : ''}` }
183 }
184 if (sub === 'usage') {
185 const max = await read($, limit)
186 const settings = `설정: 모델 ${await read($, mode)} · 자동 제안 기준 ${max === 0 ? '없음' : `${formatTokens(max)} 토큰 (fork일 때)`}`
187 const last = await read($, lastUsage)
188 if (last === null) return { text: `${settings}\n이 세션에서는 아직 다음 스텝을 만든 적이 없어요.` }
189 return {
190 text: [
191 settings,
192 `마지막 호출: ${formatUsage(last)}`,
193 `이 세션 누적 (${await read($, calls)}회): ${formatUsage(await read($, totalUsage))}`,
194 ].join('\n'),
195 }
196 }
197 if (sub !== '') return { text: HELP }
198
199 // 직접 요청은 길이 기준과 상관없이 만든다.
200 void suggest($, `manual-${await $.clock.now()}`)
201 return { text: '다음 스텝을 정리하고 있어요.' }
202 })
203
204 // 새 프롬프트가 나가면 이전 제안은 의미가 없다.
205 on('prompt.submit', async ($, e, next) => {
206 await clear($)
207 return next(e)
208 })
209
210 on('turn.complete', async ($, e, next) => {
211 const done = await next(e)
212 const isMainAnswer = e.agentId === undefined && e.reason === 'answer'
213 if (isMainAnswer && (await read($, isOn)) && !(await isTooLong($))) {
214 // 턴 종료를 붙잡지 않도록 제안 생성은 기다리지 않는다.
215 void suggest($, e.turnId)
216 }
217
218 return done
219 })
220
221 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
222 // 다른 모드(context-bar 등)가 그린 밴드. 제안이 있으면 그 아래에 쌓는다.
223 const below = await next(e)
224 if (e.props.hasSurvey || e.props.isWorking) return below
225
226 const now = await read($, phase)
227 if (now === 'idle') return below
228
229 const { Box, Button, Text } = $.ui.resolve(e)
230 const withBelow = (mine: RenderElement) => (
231 <Box flexDirection="column">
232 {mine}
233 {below}
234 </Box>
235 )
236
237 if (now === 'loading') return withBelow(<Text dimColor>다음 스텝 정리 중…</Text>)
238 if (now === 'skipped') {
239 const tokens = formatTokens(await read($, contextTokens))
240 return withBelow(
241 <Text dimColor>
242 대화가 길어(약 {tokens} 토큰) 자동 제안을 건너뛰었어요 · /next-steps로 직접 요청
243 </Text>,
244 )
245 }
246
247 const list = await read($, steps)
248 const chosen = await read($, picked)
249 const usage = await read($, lastUsage)
250 const current = await read($, mode)
251 const look = await read($, ui)
252
253 return withBelow(
254 <Box flexDirection="column" gap={look.gap}>
255 <Text dimColor>다음 스텝 — 숫자로 고르고 0으로 입력창에 채우기</Text>
256 {list.map((step, i) => (
257 <Button
258 key={`step-${i}`}
259 plain
260 hotkey={String(i + 1)}
261 onPress={() => update($, picked, list => toggle(list, i))}
262 >
263 <Text bold={look.bold}>
264 {chosen.includes(i) ? '[x] ' : '[ ] '}
265 {step}
266 </Text>
267 </Button>
268 ))}
269 <Box flexDirection="row">
270 <Button
271 key="fill"
272 variant="primary"
273 hotkey="0"
274 label={`입력창에 채우기 (${chosen.length})`}
275 onPress={() => fill($)}
276 />
277 <Text> </Text>
278 <Button key="dismiss" role="dismiss" label="닫기" onPress={() => clear($)} />
279 </Box>
280 {look.tokens && usage !== null && (
281 <Text dimColor>
282 토큰 {compactUsage(usage)} · {current}
283 </Text>
284 )}
285 </Box>,
286 )
287 })
288}
289hooks/steps.ts 135 lines1import type { UiSettings, Usage } from '../types'
2
3export const DEFAULT_UI: UiSettings = { count: 4, bold: true, gap: 1, tokens: true }
4// 항목 단축키가 숫자 한 자리(1~9)라서 9개까지.
5export const MAX_COUNT = 9
6export const MAX_GAP = 2
7
8// count는 상한일 뿐이다. 모델은 "최대 N개"를 꽉 채우려 해서 채우지 말라고 따로 못 박는다.
9export const askFor = (count: number) => `지금까지의 대화를 바탕으로, 사용자가 바로 이어서 요청할 만한 다음 작업을 제안해. 많아야 ${count}개.
10- 확실히 유용한 것만 넣는다. 개수를 채우려고 만들지 말 것. 1~2개만 유용하면 1~2개만 낸다.
11- 유용하다는 건 방금 대화의 흐름에서 사용자가 실제로 다음에 할 법한 일이라는 뜻이다. 막연한 일반론(문서화·테스트·리팩터링 등)을 대화와 상관없이 덧붙이지 않는다.
12- 각 항목은 사용자가 그대로 AI에게 보낼 수 있는 한 문장짜리 요청문(명령형)으로, 60자 안팎.
13- 대화에서 쓰인 언어로 쓴다.
14- 서로 겹치지 않게, 가치가 큰 순서로.
15- 이어서 할 일이 없거나 대화가 마무리됐으면 빈 배열.
16JSON 문자열 배열 하나만 출력하고 다른 말은 하지 마.`
17
18// 모델 답에서 첫 JSON 배열을 꺼낸다. 코드 펜스나 앞뒤 군말이 붙어도 견딘다.
19export function parseSteps(text: string, max = DEFAULT_UI.count): string[] {
20 const found = text.match(/\[[\s\S]*\]/)
21 if (!found) return []
22 try {
23 const value: unknown = JSON.parse(found[0])
24 if (!Array.isArray(value)) return []
25 return value
26 .filter((step): step is string => typeof step === 'string')
27 .map(step => step.trim())
28 .filter(step => step.length > 0)
29 .slice(0, max)
30 } catch {
31 return []
32 }
33}
34
35export function composePrompt(steps: string[]): string {
36 if (steps.length === 1) return steps[0] ?? ''
37 return ['다음을 순서대로 진행해 줘:', ...steps.map((step, i) => `${i + 1}. ${step}`)].join('\n')
38}
39
40export function toggle(picked: number[], index: number): number[] {
41 return picked.includes(index)
42 ? picked.filter(i => i !== index)
43 : [...picked, index].sort((a, b) => a - b)
44}
45
46export const NO_USAGE: Usage = {
47 input_tokens: 0,
48 output_tokens: 0,
49 cache_read_input_tokens: 0,
50 cache_creation_input_tokens: 0,
51}
52
53export function addUsage(a: Usage, b: Usage): Usage {
54 return {
55 input_tokens: a.input_tokens + b.input_tokens,
56 output_tokens: a.output_tokens + b.output_tokens,
57 cache_read_input_tokens: a.cache_read_input_tokens + b.cache_read_input_tokens,
58 cache_creation_input_tokens: a.cache_creation_input_tokens + b.cache_creation_input_tokens,
59 }
60}
61
62export function formatUsage(u: Usage): string {
63 const n = (v: number) => v.toLocaleString('en-US')
64 return `입력 ${n(u.input_tokens)} · 캐시 읽기 ${n(u.cache_read_input_tokens)} · 캐시 쓰기 ${n(u.cache_creation_input_tokens)} · 출력 ${n(u.output_tokens)}`
65}
66
67// fork 모드에서 컨텍스트가 이보다 크면 자동 제안을 건너뛴다. 0이면 건너뛰지 않는다.
68export const DEFAULT_LIMIT = 100_000
69
70const PER_MESSAGE_CHARS = 2_000
71const TRANSCRIPT_CHARS = 24_000
72
73// Haiku에 보낼 최근 대화. 뒤에서부터 메시지당·전체 글자 수를 잘라 비용을 일정하게 둔다.
74export function recentTranscript(messages: readonly { role: 'user' | 'assistant'; text: string }[]): string {
75 const lines: string[] = []
76 let size = 0
77 for (const message of [...messages].reverse()) {
78 const text = message.text.trim()
79 if (text === '') continue
80 const cut = text.length > PER_MESSAGE_CHARS ? `${text.slice(0, PER_MESSAGE_CHARS)}…` : text
81 const line = `${message.role === 'user' ? '사용자' : 'AI'}: ${cut}`
82 if (size + line.length > TRANSCRIPT_CHARS) break
83 lines.unshift(line)
84 size += line.length
85 }
86 return lines.join('\n\n')
87}
88
89export function formatTokens(n: number): string {
90 if (n < 1_000) return String(n)
91 return `${(n / 1_000).toFixed(n < 10_000 ? 1 : 0)}k`
92}
93
94// 밴드 한 줄용: 보낸 토큰 전체(그중 캐시 읽기)와 받은 토큰.
95export function compactUsage(u: Usage): string {
96 const sent = u.input_tokens + u.cache_read_input_tokens + u.cache_creation_input_tokens
97 const cached = u.cache_read_input_tokens > 0 ? ` (캐시 ${formatTokens(u.cache_read_input_tokens)})` : ''
98 return `입력 ${formatTokens(sent)}${cached} · 출력 ${formatTokens(u.output_tokens)}`
99}
100
101// $.store에서 읽은 값을 믿지 않고 범위 안의 설정으로 맞춘다.
102export function normalizeUi(value: unknown): UiSettings {
103 const v = (typeof value === 'object' && value !== null ? value : {}) as Partial<Record<keyof UiSettings, unknown>>
104 const int = (x: unknown, min: number, max: number, fallback: number) =>
105 typeof x === 'number' && Number.isInteger(x) && x >= min && x <= max ? x : fallback
106 return {
107 count: int(v.count, 1, MAX_COUNT, DEFAULT_UI.count),
108 bold: typeof v.bold === 'boolean' ? v.bold : DEFAULT_UI.bold,
109 gap: int(v.gap, 0, MAX_GAP, DEFAULT_UI.gap),
110 tokens: typeof v.tokens === 'boolean' ? v.tokens : DEFAULT_UI.tokens,
111 }
112}
113
114// `/next-steps ui <key> <value>` 한 건을 적용한다. 값이 틀리면 이유 문자열을 돌려준다.
115export function applyUi(ui: UiSettings, key: string, value: string): UiSettings | string {
116 const onOff = value === 'on' ? true : value === 'off' ? false : undefined
117 const n = Number(value)
118 switch (key) {
119 case 'count':
120 return value !== '' && Number.isInteger(n) && n >= 1 && n <= MAX_COUNT ? { ...ui, count: n } : `count는 1~${MAX_COUNT} 사이 정수예요.`
121 case 'gap':
122 return value !== '' && Number.isInteger(n) && n >= 0 && n <= MAX_GAP ? { ...ui, gap: n } : `gap은 0~${MAX_GAP} 사이 정수예요.`
123 case 'bold':
124 return onOff === undefined ? 'bold는 on 또는 off예요.' : { ...ui, bold: onOff }
125 case 'tokens':
126 return onOff === undefined ? 'tokens는 on 또는 off예요.' : { ...ui, tokens: onOff }
127 default:
128 return `바꿀 수 있는 항목은 count·bold·gap·tokens예요. 글자 크기는 앱(Cmd +)이나 터미널 글꼴 설정으로 바꿔 주세요.`
129 }
130}
131
132export function formatUi(ui: UiSettings): string {
133 return `항목 수 ${ui.count} · 굵게 ${ui.bold ? 'on' : 'off'} · 간격 ${ui.gap} · 토큰 줄 ${ui.tokens ? 'on' : 'off'}`
134}
135types/index.d.ts 39 lines1export type Phase = 'idle' | 'loading' | 'ready' | 'skipped'
2
3// haiku: 최근 대화만 잘라 Haiku에 보낸다(싸고 비용이 일정). fork: 대화 전체를 세션 모델로(정확하지만 길수록 비쌈).
4export type Mode = 'haiku' | 'fork'
5
6// 다음 스텝 밴드 표시 설정. 글자 크기는 화면(앱·터미널)이 정해서 여기 없다.
7export type UiSettings = {
8 count: number
9 bold: boolean
10 gap: number
11 tokens: boolean
12}
13
14export type Usage = {
15 input_tokens: number
16 output_tokens: number
17 cache_read_input_tokens: number
18 cache_creation_input_tokens: number
19}
20
21declare module 'claude-code' {
22 interface PluginState {
23 'next-steps': {
24 phase: Phase
25 steps: string[]
26 picked: number[]
27 turnId: string
28 isOn: boolean
29 lastUsage: Usage | null
30 totalUsage: Usage
31 calls: number
32 mode: Mode
33 limit: number
34 contextTokens: number
35 ui: UiSettings
36 }
37 }
38}
39