Your prompts as snippets: ;name in the prompt expands to the saved text, /snip save keeps the last prompt, /snip lists them all / Ваши промпты как сниппеты…

Your prompts as snippets: ;name in the prompt expands to the saved text, /snip save keeps the last prompt, /snip lists them all.
The same requests come back every day: a review checklist, "fix the failing test and run it again", a project's conventions. Save each once and type it in two keystrokes.
/snip save review keeps the last prompt you sent as ;review; /snip save fix Fix the failing test, then run it again. saves the text given right there (lines included).; and the first letters: the suggestions list the matching snippets with their first line; taking one puts the whole text in its place. A ;review sent as it is expands too, so ;review for a.ts works without the suggestion./snip opens a pane with every snippet, Insert (at the cursor, the draft stays) and Delete. /snip rm review deletes one.Snippets are yours across every project and session. Names are letters (any script), digits, _ and -, so ;ревью works too.
Keeps the snippets and the last prompt you typed in the mod's store (a JSON file under the Claude Code configuration folder). Adds suggestions for words starting with ;; replaces a sent ;name with the snippet's text. Adds the /snip command.
/config)language: auto (default; follows LANG, else the Windows UI language or regional format), ru or en./plugin install prompt-snippets --marketplace ArtyaEXE/claude-mods
Part of claude-mods. MIT license.
Ваши промпты как сниппеты: ;имя в промпте разворачивается в сохранённый текст, /snip save сохраняет последний промпт, /snip показывает все.
Одни и те же просьбы возвращаются каждый день: чек-лист ревью, «почини упавший тест и прогони заново», соглашения проекта. Сохраните каждую один раз и вставляйте в два нажатия.
/snip save ревью сохраняет последний отправленный промпт как ;ревью; /snip save fix Почини упавший тест и прогони заново. сохраняет текст, указанный прямо в команде (с переносами строк).; и первые буквы — в подсказках появятся подходящие сниппеты с первой строкой; выбор подставляет весь текст на место слова. Отправленный как есть ;ревью тоже разворачивается, так что ;ревью для a.ts работает и без подсказки./snip открывает панель со всеми сниппетами, кнопками Вставить (у курсора, набранное остаётся) и Удалить. /snip rm ревью удаляет один.Сниппеты общие для всех проектов и сессий. Имя — буквы (любого алфавита), цифры, _ и -, так что ;ревью тоже работает.
Хранит сниппеты и последний набранный промпт в хранилище мода (JSON-файл в папке настроек Claude Code). Добавляет подсказки для слов, начинающихся с ;; заменяет отправленное ;имя текстом сниппета. Добавляет команду /snip.
/config)language: auto (по умолчанию; по LANG, иначе по языку интерфейса или региональному формату Windows), ru или en./plugin install prompt-snippets --marketplace ArtyaEXE/claude-mods
Часть claude-mods. Лицензия MIT.
hooks/register.tsx 184 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Snippet } from '../types'
5
6type Lang = 'ru' | 'en'
7
8// `ru` / `en` as set; `auto`: the locale variables first, then Windows' UI language or regional format.
9async function resolveLang($: EngineInterface, setting: unknown): Promise<Lang> {
10 if (setting === 'ru' || setting === 'en') return setting
11 const env = (await $.env.get('LC_ALL')) || (await $.env.get('LC_MESSAGES')) || (await $.env.get('LANG'))
12 if (env) return env.toLowerCase().startsWith('ru') ? 'ru' : 'en'
13 try {
14 const ran = await $.process.run(
15 ['powershell.exe', '-NoProfile', '-NonInteractive', '-Command', '(Get-UICulture).Name; (Get-Culture).Name'],
16 { timeoutMs: 10_000 },
17 )
18 if (ran.exitCode === 0 && /^ru\b/im.test(ran.stdout)) return 'ru'
19 } catch {
20 // Not Windows: English.
21 }
22
23 return 'en'
24}
25
26const PANE = 'prompt-snippets'
27const list = atom({ plugin: 'prompt-snippets', key: 'list' } as const, [] as Snippet[], { shape: 'list-1' })
28
29// A snippet's name: letters (any script), digits, `_` and `-`.
30const NAME = /^[\p{L}\p{N}_-]+$/u
31// `;name` standing alone in a prompt: at the start or after whitespace, up to whitespace or the end.
32const MENTION = /(^|\s);([\p{L}\p{N}_-]+)(?=\s|$)/gu
33
34const STRINGS = {
35 ru: {
36 command: 'Сниппеты промптов: ;имя в промпте; /snip save имя [текст] | rm имя',
37 paneTitle: 'Сниппеты',
38 opened: 'Открыл панель сниппетов.',
39 empty: 'Сниппетов нет. Отправьте промпт и сохраните его: /snip save имя — или сразу с текстом: /snip save имя текст.',
40 hint: 'В промпте: ;имя — подсказка подставит текст',
41 insert: 'Вставить',
42 remove: 'Удалить',
43 saved: (name: string, n: number) => `Сохранил ;${name} (${n} симв.). В промпте: ;${name}`,
44 replaced: (name: string) => `Перезаписал ;${name}.`,
45 removed: (name: string) => `Удалил ;${name}.`,
46 noSuch: (name: string) => `Сниппета ;${name} нет.`,
47 noLast: 'Пока нечего сохранять: отправьте промпт, затем /snip save имя. Или: /snip save имя текст.',
48 badName: (name: string) => `Имя «${name}» не подходит: только буквы, цифры, _ и -.`,
49 usage: 'Использование: /snip, /snip save имя [текст], /snip rm имя',
50 },
51 en: {
52 command: 'Prompt snippets: ;name in the prompt; /snip save name [text] | rm name',
53 paneTitle: 'Snippets',
54 opened: 'Opened the snippets pane.',
55 empty: 'No snippets yet. Send a prompt and keep it: /snip save name — or with the text right away: /snip save name text.',
56 hint: 'In a prompt: ;name — the suggestion puts the text in',
57 insert: 'Insert',
58 remove: 'Delete',
59 saved: (name: string, n: number) => `Saved ;${name} (${n} chars). In a prompt: ;${name}`,
60 replaced: (name: string) => `Replaced ;${name}.`,
61 removed: (name: string) => `Deleted ;${name}.`,
62 noSuch: (name: string) => `There is no ;${name}.`,
63 noLast: 'Nothing to save yet: send a prompt, then /snip save name. Or: /snip save name text.',
64 badName: (name: string) => `"${name}" will not do as a name: letters, digits, _ and - only.`,
65 usage: 'Usage: /snip, /snip save name [text], /snip rm name',
66 },
67}
68
69let lang: Lang = 'en'
70
71type Saved = Record<string, string>
72
73async function load($: EngineInterface): Promise<Saved> {
74 const raw = await $.store.get('snippets')
75
76 return raw !== null && typeof raw === 'object' ? (raw as Saved) : {}
77}
78
79async function store($: EngineInterface, saved: Saved) {
80 await $.store.set('snippets', saved)
81 await update($, list, () =>
82 Object.entries(saved)
83 .map(([name, text]) => ({ name, text }))
84 .sort((a, b) => a.name.localeCompare(b.name)),
85 )
86}
87
88const firstLine = (text: string, n = 70) => {
89 const line = text.split('\n').find(l => l.trim() !== '')?.trim() ?? ''
90
91 return line.length > n ? `${line.slice(0, n - 1)}…` : line
92}
93
94async function remove($: EngineInterface, name: string) {
95 const saved = await load($)
96 delete saved[name]
97 await store($, saved)
98}
99
100export const register: Register = (on, options) => {
101 on('session.start', async ($, e, next) => {
102 const started = await next(e)
103 lang = await resolveLang($, (options as { language?: unknown }).language)
104 await $.command.register({ name: 'snip', description: STRINGS[lang].command, argumentHint: 'save <name> [text] | rm <name>' })
105 await store($, await load($))
106
107 return started
108 })
109
110 on('command.run', { command: 'snip' }, async ($, e) => {
111 const L = STRINGS[lang]
112 const [verb = '', name = '', ...rest] = e.args.trim().split(/\s+/)
113 if (verb === '') {
114 await $.ui.open({ id: PANE, title: L.paneTitle, focus: true, closeOnEscape: true })
115
116 return { text: L.opened }
117 }
118 if (verb !== 'save' && verb !== 'rm') return { text: L.usage }
119 if (!NAME.test(name)) return { text: name === '' ? L.usage : L.badName(name) }
120
121 const saved = await load($)
122 if (verb === 'rm') {
123 if (saved[name] === undefined) return { text: L.noSuch(name) }
124 await remove($, name)
125
126 return { text: L.removed(name) }
127 }
128 // The text after the name as typed (spacing and lines kept), else the last prompt sent.
129 const given = rest.length > 0 ? e.args.trim().replace(/^\S+\s+\S+\s*/, '') : ''
130 const last = await $.store.get('lastPrompt')
131 const text = given !== '' ? given : typeof last === 'string' ? last : ''
132 if (text === '') return { text: L.noLast }
133 const existed = saved[name] !== undefined
134 await store($, { ...saved, [name]: text })
135
136 return { text: existed ? L.replaced(name) : L.saved(name, text.length) }
137 })
138
139 // `;na…` while typing: the snippets whose names start so, each writing its whole text in place of the token.
140 on('prompt.autocomplete', { token: /^;/ }, async ($, e, next) => {
141 const below = await next(e)
142 const typed = e.token.slice(1).toLowerCase()
143 const mine = (await read($, list))
144 .filter(s => s.name.toLowerCase().startsWith(typed))
145 .map(s => ({ text: s.text, label: `;${s.name}`, description: firstLine(s.text) }))
146
147 return { suggestions: [...mine, ...below.suggestions] }
148 })
149
150 // A `;name` sent as it is expands to its text; a prompt typed by the person is kept as the last one, for /snip save.
151 on('prompt.submit', async ($, e, next) => {
152 // The person's own words: typed here, or sent from the phone through Remote Control.
153 if ((e.origin.kind !== 'composer' && e.origin.kind !== 'bridge') || e.text.trimStart().startsWith('/')) return next(e)
154 const saved = await load($)
155 const text = e.text.replace(MENTION, (whole, lead: string, name: string) => (saved[name] === undefined ? whole : `${lead}${saved[name]}`))
156 await $.store.set('lastPrompt', e.text)
157
158 return next(text === e.text ? e : { ...e, text })
159 })
160
161 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
162 const { Box, Button, Text } = $.ui.resolve(e)
163 const L = STRINGS[lang]
164 const snippets = await read($, list)
165 const width = Math.max(20, e.props.bodyColumns - 30)
166
167 return (
168 <Box flexDirection="column">
169 {snippets.length === 0 ? <Text dimColor>{L.empty}</Text> : <Text dimColor>{L.hint}</Text>}
170 {snippets.map(s => (
171 <Box flexDirection="row" gap={1}>
172 <Text color="cyan">;{s.name}</Text>
173 <Text dimColor wrap="truncate">
174 {firstLine(s.text, width)}
175 </Text>
176 <Button key={`insert:${s.name}`} plain label={L.insert} onPress={() => void $.prompt.fill({ text: s.text, mode: 'insert' })} />
177 <Button key={`remove:${s.name}`} plain dimColor label={L.remove} onPress={() => remove($, s.name)} />
178 </Box>
179 ))}
180 </Box>
181 )
182 })
183}
184types/index.d.ts 11 lines1export type Snippet = { name: string; text: string }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'prompt-snippets': {
6 // The saved snippets as the pane draws them, by name.
7 list: Shaped<Snippet[]>
8 }
9 }
10}
11