Rewrites each prompt so the AI understands it better, shows it above the prompt box, and sends the rewrite only if you allow it

Claude Code mod 모음. 이 저장소 자체가 마켓플레이스(.claude-plugin/marketplace.json)라서 누구나 아래 방법으로 설치할 수 있다.
| Mod | 설명 |
|---|---|
context-bars | 컨텍스트 윈도우를 카테고리별 비례 막대 하나로 프롬프트 위에 표시 |
token-weather | 컨텍스트 윈도우 사용량을 날씨 아이콘·퍼센트·최근 턴 차트로 프롬프트 위에 표시 |
y-change | Claude가 코드를 수정할 때마다 diff와 변경 이유(처음 보는 언어의 문법 설명 포함)를 오른쪽 pane에 표시 (기본은 숨김, /y-change로 열기/닫기) |
enhance-prompt | 입력한 프롬프트를 AI가 더 잘 알아듣도록 [THE JOB]/[THE WHY]/[GUARDRAILS]/[DONE MEAN] 구조로 개선해 입력창 위에 보여주고(불명확한 부분은 먼저 입력창 위 질문으로 되묻고 답하면 반복, 최대 3개), [허용]하면 개선본을, [거부]하면 원본을 전송. enhance-prompt: ON/OFF 버튼(데스크탑 입력창 위)을 눌러, 또는 /enhance-prompt로 켜기/끄기 |
task-progress | Claude의 작업 목록(TodoWrite·TaskCreate/TaskUpdate) 진행률을 막대와 완료/전체, 진행 중인 항목 이름으로 프롬프트 위(스피너 아래)에 표시 |
mod-switch | 설치된 mod 목록을 오른쪽 pane에 토글 스위치와 함께 표시하고, 스위치를 누르면 해당 mod를 켜거나 끔 (기본은 숨김, /mod-switch로 열기/닫기) |
터미널의 Claude Code 세션 프롬프트에서 입력한다. 마켓플레이스 등록과 설치가 한 번에 된다.
/plugin install context-bars --marketplace nalpari/claude-mods
/plugin install token-weather --marketplace nalpari/claude-mods
/plugin install y-change --marketplace nalpari/claude-mods
/plugin install task-progress --marketplace nalpari/claude-mods
/plugin install enhance-prompt --marketplace nalpari/claude-mods
/plugin install mod-switch --marketplace nalpari/claude-mods
처음이면 Add marketplace? 에 y 로 답하고 scope(user 권장)를 고르면 Installed <mod>. Plugin is now active. 가 뜬다. 재시작이나 리로드 없이 바로 적용되고, user scope 는 이후 세션에도 유지된다. 마켓플레이스를 이미 추가했다면 확인 질문은 건너뛴다.
마켓플레이스를 먼저 등록하고 나중에 골라 설치해도 된다.
/plugin marketplace add nalpari/claude-mods
/plugin install context-bars@claude-mods
셸에서는 claude plugin marketplace add nalpari/claude-mods 와 claude plugin install context-bars@claude-mods --scope user 로 같은 일을 한다.
claude plugin marketplace update claude-mods # 마켓플레이스 최신화
claude plugin update context-bars@claude-mods # mod 업데이트
claude plugin list # 설치 확인
claude plugin uninstall context-bars # 제거
git clone https://github.com/nalpari/claude-mods.git
cd claude-mods
# 이번 세션에서만 시험
claude --plugin-dir ./context-bars
# 로컬 마켓플레이스로 설치
claude plugin marketplace add ./
claude plugin install context-bars@claude-mods --scope user
token-weather/README.md 기준)./plugin install 은 터미널 전용이다. 데스크톱 앱 Code 탭에서는 쓸 수 없지만, 터미널에서 user scope 로 설치하면 로컬 세션에서도 로드된다.hooks/register.tsx 232 lines1import type { EngineInterface, PromptSubmitInput, Register } from 'claude-code'
2
3type Clarification = { question: string; answer: string }
4type Stage =
5 | { kind: 'idle' }
6 | { kind: 'enhancing' }
7 | { kind: 'asking'; original: string; asked: Clarification[]; question: string }
8 | { kind: 'review'; original: string; enhanced: string }
9// what the model sends back: one more question, or the improved prompt
10type Reply = { question: string } | { enhanced: string }
11
12// prompts outside these bounds go through as typed: "ok" has nothing to improve, a pasted log would be echoed back whole
13const MIN_CHARS = 8
14const MAX_CHARS = 2000
15// a model that keeps asking would never get to the review
16const MAX_QUESTIONS = 3
17const QUESTION = /^QUESTION:\s*(\S[\s\S]*)/
18
19const ask = (text: string, asked: Clarification[]) => `[enhance-prompt] Rewrite the prompt below, which the user is about to send, so an AI coding assistant understands it better.
20
21- Keep the user's intent, scope and language. Do not answer it, and do not add requirements the user did not state.
22- Use this conversation${asked.length > 0 ? ' and the clarifications' : ''} to spell out what the prompt leaves implicit: what "it" or "that" refers to, which files or functions, the result expected, the constraints already stated.
23- Keep code, paths, identifiers and quoted text exactly as written.
24${asked.length < MAX_QUESTIONS
25 ? '- If an essential detail is still unclear and nothing above settles it, reply with one line and nothing else: QUESTION: <one short question, in the user\'s language>. Ask only what would change the rewritten prompt; if nothing essential is unclear, do not ask.'
26 : '- Do not ask any more questions: write the best prompt you can from what you have.'}
27- Otherwise reply with the rewritten prompt only: no preface, no quotes, no explanation. Use exactly this structure, with the four headings in English as written and what is under them in the user's language. Fill each section only from what the prompt, this conversation and the clarifications say; if a section has nothing, write "-" under it.
28
29[THE JOB]
30Description of the task
31
32[THE WHY]
33Reason or context for the task
34
35[GUARDRAILS]
36Constraints and guidelines
37
38[DONE MEAN]
39Completion criteria
40
41<prompt>
42${text}
43</prompt>${asked.length > 0 ? `
44
45<clarifications>
46${asked.map(c => `Q: ${c.question}\nA: ${c.answer}`).join('\n')}
47</clarifications>` : ''}`
48
49// what is held between Enter and the person's decision; a module variable like task-progress's list, so a reload drops a review in progress
50let stage: Stage = { kind: 'idle' }
51let isOn = true
52
53const setStage = ($: EngineInterface, next: Stage) => {
54 stage = next
55 $.ui.invalidate('ui.render')
56}
57
58const toggle = async ($: EngineInterface) => {
59 isOn = !isOn
60 await $.store.set('isOn', isOn)
61 $.ui.invalidate('ui.render')
62}
63
64// only the person's own Enter, outside a running turn, on plain text
65const isTyped = (e: PromptSubmitInput) =>
66 e.origin.kind === 'composer'
67 && e.turnId === undefined
68 // the held prompt is sent again by the plugin, which expands neither images nor @mentions
69 && e.attachments === undefined
70 && !/^[/!]/.test(e.text.trim())
71
72const isSkipped = (e: PromptSubmitInput) => {
73 const text = e.text.trim()
74
75 return !isTyped(e)
76 || /(^|\s)@\S/.test(text)
77 || text.length < MIN_CHARS
78 || text.length > MAX_CHARS
79}
80
81// the next question or the improved prompt, or undefined to send the prompt as typed
82const enhance = async ($: EngineInterface, text: string, asked: Clarification[]): Promise<Reply | undefined> => {
83 const prompt = ask(text, asked)
84 let r = await $.model.fork({ prompt })
85 // nothing to fork from before the conversation's first reply, and right after /clear: ask with no history
86 if (!r.isAnswered && r.reason === 'nothing-to-fork') {
87 r = await $.model.complete({ model: 'haiku', prompt, maxTokens: 2048, timeoutMs: 30_000 })
88 }
89 const fail = (reason: string) => {
90 $.ui.toast(`enhance-prompt: 개선하지 못해 원본을 보냅니다 (${reason})`)
91
92 return undefined
93 }
94 if (!r.isAnswered) return fail(r.reason)
95 const reply = r.text.trim()
96 const question = QUESTION.exec(reply)?.[1].trim()
97 if (question !== undefined) return asked.length < MAX_QUESTIONS ? { question } : fail('too-many-questions')
98
99 return reply === '' || reply === text.trim() ? undefined : { enhanced: reply }
100}
101
102// asks the model once more with everything answered so far; the drop that holds the prompt, or undefined to send it as typed
103const converse = async ($: EngineInterface, original: string, asked: Clarification[]) => {
104 setStage($, { kind: 'enhancing' })
105 let reply: Reply | undefined
106 try {
107 reply = await enhance($, original, asked)
108 } finally {
109 if (reply === undefined) setStage($, { kind: 'idle' })
110 }
111 if (reply === undefined) return undefined
112
113 // the drop leaves what was typed in the box; the question or review shows it as 원본, so empty the box before it is drawn
114 await $.prompt.fill({ text: '' })
115 if ('question' in reply) {
116 setStage($, { kind: 'asking', original, asked, question: reply.question })
117
118 return { drop: '입력창 위의 질문에 답해 주세요.' }
119 }
120 setStage($, { kind: 'review', original, enhanced: reply.enhanced })
121
122 return { drop: '개선된 프롬프트를 입력창 위에서 확인해 주세요.' }
123}
124
125// cleared first, so a second press cannot send it twice; a prompt that cannot be sent goes back to the box
126const decide = async ($: EngineInterface, text: string) => {
127 setStage($, { kind: 'idle' })
128 await $.prompt.submit({ text, asUser: true }).catch(() => $.prompt.fill({ text }))
129}
130
131export const register: Register = on => {
132 on('session.start', async ($, e, next) => {
133 await $.command.register({ name: 'enhance-prompt', description: '프롬프트 개선 켜기/끄기' })
134 isOn = (await $.store.get('isOn')) !== false
135 setStage($, { kind: 'idle' })
136
137 return next(e)
138 })
139
140 // /clear (or a resume) ends the session and starts the next one without a session.start: a held prompt is not the new session's
141 on('session.end', ($, e, next) => {
142 setStage($, { kind: 'idle' })
143
144 return next(e)
145 })
146
147 on('command.run', { command: 'enhance-prompt' }, async $ => {
148 await toggle($)
149
150 return { text: `프롬프트 개선을 ${isOn ? '켰' : '껐'}습니다.` }
151 })
152
153 // an open question takes the person's next Enter as its answer; a review or an enhancement already under way leaves later prompts alone: they go as typed
154 on('prompt.submit', async ($, e, next) => {
155 const held = stage
156 if (held.kind === 'asking' && isTyped(e)) {
157 const asked = [...held.asked, { question: held.question, answer: e.text.trim() }]
158
159 return (await converse($, held.original, asked)) ?? next({ ...e, text: held.original })
160 }
161 if (!isOn || held.kind !== 'idle' || isSkipped(e)) return next(e)
162
163 return (await converse($, e.text, [])) ?? next(e)
164 })
165
166 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
167 // the band is one instance: a hook that answers without next hides every other mod's band, so draw above what is beneath
168 const below = await next(e)
169 const held = stage
170 if (e.props.hasSurvey || held.kind === 'idle') return below
171
172 const { Box, Button, Text } = $.ui.resolve(e)
173 const title = <Text color="claude">enhance-prompt</Text>
174
175 if (held.kind === 'enhancing') {
176 return (
177 <Box flexDirection="column">
178 <Text wrap="truncate">{title}<Text dimColor>{' 프롬프트를 개선하는 중…'}</Text></Text>
179 {below}
180 </Box>
181 )
182 }
183
184 if (held.kind === 'asking') {
185 return (
186 <Box flexDirection="column">
187 <Text>{title}{` ${held.question}`}</Text>
188 <Text dimColor>{`원본 ${held.original}`}</Text>
189 <Box>
190 <Button key="skip" label="건너뛰기" onPress={() => decide($, held.original)} />
191 <Text dimColor>{' 입력창에 답하고 Enter · 건너뛰면 입력한 원본을 보냅니다'}</Text>
192 </Box>
193 {below}
194 </Box>
195 )
196 }
197
198 return (
199 <Box flexDirection="column">
200 <Text>{title}{' 개선된 프롬프트를 보낼까요?'}</Text>
201 <Text dimColor>{`원본 ${held.original}`}</Text>
202 <Text>{`개선 ${held.enhanced}`}</Text>
203 <Box>
204 <Button key="accept" label="허용" hotkey="y" variant="primary" onPress={() => decide($, held.enhanced)} />
205 <Text>{' '}</Text>
206 <Button key="reject" label="거부" hotkey="n" onPress={() => decide($, held.original)} />
207 <Text dimColor>{' 거부하면 입력한 원본을 보냅니다'}</Text>
208 </Box>
209 {below}
210 </Box>
211 )
212 })
213
214 // the desktop's footer is a chip that is there only while a mode is on, so there the switch sits in the band above the prompt;
215 // the terminal has none: mod-switch's pane or /enhance-prompt switches it
216 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
217 const below = await next(e)
218 if (e.surface === 'terminal' || e.props.hasSurvey) return below
219
220 const { Box, Button } = $.ui.resolve(e)
221
222 return (
223 <Box flexDirection="column">
224 {below}
225 <Box>
226 <Button key="toggle" label={`enhance-prompt: ${isOn ? 'ON' : 'OFF'}`} variant={isOn ? 'primary' : 'secondary'} onPress={() => toggle($)} />
227 </Box>
228 </Box>
229 )
230 })
231}
232