dsh-output-styles 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.es.md +197 -0
- package/README.ja.md +197 -0
- package/README.ko.md +197 -0
- package/README.md +197 -0
- package/README.zh.md +197 -0
- package/cordis.patch.yml +40 -0
- package/docs/VERIFICATION.zh.md +93 -0
- package/lib/client.js +83 -0
- package/lib/index.js +415 -0
- package/lib/invariant-LV6hQX5s.js +442 -0
- package/lib/invariant.js +2 -0
- package/lib/types/client/index.d.ts +30 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/locales.d.ts +13 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/config.d.ts +73 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/index.d.ts +31 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/invariant.d.ts +59 -0
- package/lib/types/invariant.d.ts.map +1 -0
- package/lib/types/runtime.d.ts +144 -0
- package/lib/types/runtime.d.ts.map +1 -0
- package/lib/types/style-command.d.ts +68 -0
- package/lib/types/style-command.d.ts.map +1 -0
- package/lib/types/style-library.d.ts +78 -0
- package/lib/types/style-library.d.ts.map +1 -0
- package/lib/types/types.d.ts +78 -0
- package/lib/types/types.d.ts.map +1 -0
- package/package.json +138 -0
- package/src/client/index.ts +104 -0
- package/src/client/locales.ts +14 -0
- package/src/config.ts +110 -0
- package/src/index.ts +51 -0
- package/src/invariant.ts +144 -0
- package/src/runtime.ts +439 -0
- package/src/style-command.ts +89 -0
- package/src/style-library.ts +348 -0
- package/src/types.ts +86 -0
- package/styles/concise.md +17 -0
- package/styles/explanatory.md +14 -0
- package/styles/formal.md +14 -0
- package/styles/step-by-step.md +16 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Style-library loading: one style per `*.md` file (frontmatter + body),
|
|
3
|
+
* with optional Claude Code `outputStyles` JSON compatibility (single entry
|
|
4
|
+
* or an array of entries per file).
|
|
5
|
+
*
|
|
6
|
+
* A style file that does not parse is skipped with a warning; the plugin
|
|
7
|
+
* stays loadable. Structural ambiguity — a duplicate style name, a style
|
|
8
|
+
* named `off`, or two styles declaring `force` — fails the load because it
|
|
9
|
+
* would silently change which body gets injected.
|
|
10
|
+
* @module dsh-output-styles/style-library
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readdirSync, readFileSync } from 'node:fs'
|
|
14
|
+
import type { Dirent } from 'node:fs'
|
|
15
|
+
import { join } from 'node:path'
|
|
16
|
+
import { parse as parseYaml } from 'yaml'
|
|
17
|
+
import { OFF } from './types.ts'
|
|
18
|
+
|
|
19
|
+
/** Character set for style names: letters, digits, spaces, and hyphens only. */
|
|
20
|
+
export const STYLE_NAME_RE = /^[\p{L}\p{N} -]+$/u
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Whether a name is a legal style name and switch target: at least one
|
|
24
|
+
* letter or digit, only letters/digits/spaces/hyphens, and no leading or
|
|
25
|
+
* trailing space. Spaces are the only whitespace allowed, so a name is
|
|
26
|
+
* always a single switchable line the model can echo back. `off` passes this
|
|
27
|
+
* check but is a reserved target rejected by the library.
|
|
28
|
+
* @param name - candidate style name.
|
|
29
|
+
* @returns whether the name is legal.
|
|
30
|
+
*/
|
|
31
|
+
export function isValidStyleName(name: string): boolean {
|
|
32
|
+
if (name === '' || name !== name.trim()) return false
|
|
33
|
+
if (!STYLE_NAME_RE.test(name)) return false
|
|
34
|
+
return /[\p{L}\p{N}]/u.test(name)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** One loaded style: library metadata plus the raw injectable body. */
|
|
38
|
+
export interface OutputStyle {
|
|
39
|
+
/** Switch target accepted by `/style`; letters, digits, spaces, and hyphens. */
|
|
40
|
+
readonly name: string
|
|
41
|
+
/** One user-facing sentence on what the style does. */
|
|
42
|
+
readonly description: string
|
|
43
|
+
/** Optional guidance on when the style is useful; shown in listings. */
|
|
44
|
+
readonly whenToUse?: string
|
|
45
|
+
/** The raw directive injected into the system prompt, before truncation. */
|
|
46
|
+
readonly body: string
|
|
47
|
+
/** Library file this style came from, relative to the style directory. */
|
|
48
|
+
readonly file: string
|
|
49
|
+
/** Source format (`md` frontmatter or `json` compatibility entry). */
|
|
50
|
+
readonly format: 'md' | 'json'
|
|
51
|
+
/**
|
|
52
|
+
* Keep the harness prompt (identity, persona, tool guidance) when this
|
|
53
|
+
* style is active (Claude Code `keep-coding-instructions`). When false,
|
|
54
|
+
* the style replaces the whole system prompt; default false, matching
|
|
55
|
+
* Claude Code.
|
|
56
|
+
*/
|
|
57
|
+
readonly keepCodingInstructions: boolean
|
|
58
|
+
/** Apply this style unconditionally, overriding any session selection. */
|
|
59
|
+
readonly force: boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Report a style file the loader skipped or a tolerated oddity. */
|
|
63
|
+
type Warn = (message: string) => void
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Load the style library from one or more directories. Later directories
|
|
67
|
+
* override earlier ones on a same-named style (the Claude Code
|
|
68
|
+
* "closest-to-the-working-directory wins" rule); duplicates within one
|
|
69
|
+
* directory still fail the load. Deterministic order: directories in the
|
|
70
|
+
* given order, files within a directory sorted by code unit.
|
|
71
|
+
* @param stylesDirs - absolute directories, lowest priority first.
|
|
72
|
+
* @param options.compatJson - whether `*.json` entries are loaded.
|
|
73
|
+
* @param warn - warning sink (skipped files, unknown frontmatter keys).
|
|
74
|
+
* @returns the library keyed by style name, in directory/file order.
|
|
75
|
+
* @throws when a directory is unreadable, a style is named `off`, two files
|
|
76
|
+
* in one directory declare the same name, or two styles declare `force`.
|
|
77
|
+
*/
|
|
78
|
+
export function loadStyleLibrary(
|
|
79
|
+
stylesDirs: readonly string[],
|
|
80
|
+
options: { readonly compatJson: boolean },
|
|
81
|
+
warn: Warn,
|
|
82
|
+
): ReadonlyMap<string, OutputStyle> {
|
|
83
|
+
const styles = new Map<string, OutputStyle>()
|
|
84
|
+
for (const dir of stylesDirs) {
|
|
85
|
+
for (const [name, style] of loadStyleDir(dir, options, warn)) {
|
|
86
|
+
styles.set(name, style) // a later directory overrides an earlier one
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const forced = [...styles.values()].filter(style => style.force)
|
|
90
|
+
if (forced.length > 1) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`dsh-output-styles: styles ${forced.map(style => style.file).join(' and ')} both declare force; at most one style may be forced`,
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
return styles
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Load every style in one directory (duplicates within it fail the load). */
|
|
99
|
+
function loadStyleDir(
|
|
100
|
+
stylesDir: string,
|
|
101
|
+
options: { readonly compatJson: boolean },
|
|
102
|
+
warn: Warn,
|
|
103
|
+
): ReadonlyMap<string, OutputStyle> {
|
|
104
|
+
let entries: Dirent[]
|
|
105
|
+
try {
|
|
106
|
+
entries = readdirSync(stylesDir, { withFileTypes: true })
|
|
107
|
+
} catch (cause) {
|
|
108
|
+
throw new Error(`dsh-output-styles: style directory ${stylesDir} is unreadable`, { cause })
|
|
109
|
+
}
|
|
110
|
+
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)
|
|
111
|
+
const styles = new Map<string, OutputStyle>()
|
|
112
|
+
const addStyle = (style: OutputStyle): void => {
|
|
113
|
+
if (style.name === OFF) {
|
|
114
|
+
throw new Error(`dsh-output-styles: style ${style.file} is named "${OFF}", which is reserved for switching output styles off`)
|
|
115
|
+
}
|
|
116
|
+
if (styles.has(style.name)) {
|
|
117
|
+
throw new Error(`dsh-output-styles: duplicate style name "${style.name}" (${styles.get(style.name)?.file} and ${style.file})`)
|
|
118
|
+
}
|
|
119
|
+
styles.set(style.name, style)
|
|
120
|
+
}
|
|
121
|
+
for (const entry of entries) {
|
|
122
|
+
if (!entry.isFile()) continue
|
|
123
|
+
const file = entry.name
|
|
124
|
+
if (file.endsWith('.md')) {
|
|
125
|
+
const parsed = parseMarkdownStyle(file, readStyleFile(stylesDir, file, warn))
|
|
126
|
+
if (parsed.oddity !== undefined) warn(parsed.oddity)
|
|
127
|
+
if (parsed.problem !== undefined) {
|
|
128
|
+
warn(`skipping ${file}: ${parsed.problem}`)
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
if (parsed.style !== undefined) addStyle(parsed.style)
|
|
132
|
+
} else if (file.endsWith('.json')) {
|
|
133
|
+
if (!options.compatJson) {
|
|
134
|
+
warn(`ignoring ${file}: JSON style loading is disabled (compatJson: false)`)
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
for (const style of parseJsonFile(file, readStyleFile(stylesDir, file, warn), warn)) {
|
|
138
|
+
addStyle(style)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return styles
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Read one library file; an unreadable file is a skipped-file warning. */
|
|
146
|
+
function readStyleFile(stylesDir: string, file: string, warn: Warn): string | undefined {
|
|
147
|
+
try {
|
|
148
|
+
return readFileSync(join(stylesDir, file), 'utf8')
|
|
149
|
+
} catch (cause) {
|
|
150
|
+
warn(`skipping ${file}: unreadable (${cause instanceof Error ? cause.message : String(cause)})`)
|
|
151
|
+
return undefined
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Parse a `---`-fenced frontmatter style file. */
|
|
156
|
+
function parseMarkdownStyle(file: string, source: string | undefined): { style?: OutputStyle; problem?: string; oddity?: string } {
|
|
157
|
+
if (source === undefined) return {}
|
|
158
|
+
const open = /^---[ \t]*\r?\n/.exec(source)
|
|
159
|
+
if (open === null) {
|
|
160
|
+
return { problem: 'missing `---` frontmatter block' }
|
|
161
|
+
}
|
|
162
|
+
const close = /\r?\n---[ \t]*(?:\r?\n|$)/.exec(source.slice(open[0].length))
|
|
163
|
+
if (close === null) {
|
|
164
|
+
return { problem: 'unterminated `---` frontmatter block' }
|
|
165
|
+
}
|
|
166
|
+
const frontmatter = source.slice(open[0].length, open[0].length + close.index)
|
|
167
|
+
const bodyStart = open[0].length + close.index + close[0].length
|
|
168
|
+
const body = source.slice(bodyStart).trim()
|
|
169
|
+
const parsed = parseFrontmatter(file, frontmatter)
|
|
170
|
+
if (parsed.problem !== undefined) return { problem: parsed.problem }
|
|
171
|
+
if (body === '') {
|
|
172
|
+
return { problem: 'style body must be non-empty' }
|
|
173
|
+
}
|
|
174
|
+
const fields = parsed.fields
|
|
175
|
+
if (fields === undefined) return {}
|
|
176
|
+
const style = {
|
|
177
|
+
name: fields.name,
|
|
178
|
+
description: fields.description,
|
|
179
|
+
...fields.whenToUse === undefined ? {} : { whenToUse: fields.whenToUse },
|
|
180
|
+
body,
|
|
181
|
+
file,
|
|
182
|
+
format: 'md' as const,
|
|
183
|
+
keepCodingInstructions: fields.keepCodingInstructions,
|
|
184
|
+
force: fields.force,
|
|
185
|
+
}
|
|
186
|
+
return parsed.oddity === undefined ? { style } : { style, oddity: parsed.oddity }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Frontmatter fields shared by the two source formats. */
|
|
190
|
+
interface StyleFields {
|
|
191
|
+
name: string
|
|
192
|
+
description: string
|
|
193
|
+
whenToUse?: string
|
|
194
|
+
keepCodingInstructions: boolean
|
|
195
|
+
force: boolean
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Attach a present oddity without an explicit-undefined optional key. */
|
|
199
|
+
function withOddity<T extends object>(value: T, oddity: string | undefined): T | (T & { oddity: string }) {
|
|
200
|
+
return oddity === undefined ? value : { ...value, oddity }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The file name a `name`-less style inherits: the file name without its extension. */
|
|
204
|
+
function defaultStyleName(file: string): string {
|
|
205
|
+
return file.slice(0, file.length - '.md'.length)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Keys both frontmatter formats accept, plus the booleans they share. */
|
|
209
|
+
const FRONTMATTER_KEYS = new Set([
|
|
210
|
+
'name',
|
|
211
|
+
'description',
|
|
212
|
+
'whenToUse',
|
|
213
|
+
'keep-coding-instructions',
|
|
214
|
+
'force',
|
|
215
|
+
])
|
|
216
|
+
|
|
217
|
+
/** Validate one frontmatter block into {@link StyleFields}. */
|
|
218
|
+
function parseFrontmatter(file: string, frontmatter: string): { fields?: StyleFields; problem?: string; oddity?: string } {
|
|
219
|
+
let raw: unknown
|
|
220
|
+
try {
|
|
221
|
+
raw = parseYaml(frontmatter)
|
|
222
|
+
} catch (cause) {
|
|
223
|
+
return { problem: `frontmatter is not valid YAML (${cause instanceof Error ? cause.message : String(cause)})` }
|
|
224
|
+
}
|
|
225
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
226
|
+
return { problem: 'frontmatter must be a mapping of scalar fields' }
|
|
227
|
+
}
|
|
228
|
+
const record = raw as Record<string, unknown>
|
|
229
|
+
const oddityKeys = Object.keys(record).filter(key => !FRONTMATTER_KEYS.has(key))
|
|
230
|
+
const oddity = oddityKeys.length > 0 ? `${file}: ignoring unknown frontmatter field${oddityKeys.length > 1 ? 's' : ''} ${oddityKeys.join(', ')}` : undefined
|
|
231
|
+
const { name, description, whenToUse } = record
|
|
232
|
+
const effectiveName = name === undefined ? defaultStyleName(file) : name
|
|
233
|
+
if (typeof effectiveName !== 'string' || !isValidStyleName(effectiveName)) {
|
|
234
|
+
return withOddity({ problem: 'frontmatter name must be letters, digits, spaces, or hyphens, with at least one letter or digit and no leading/trailing space' }, oddity)
|
|
235
|
+
}
|
|
236
|
+
if (typeof description !== 'string' || description.trim() === '') {
|
|
237
|
+
return withOddity({ problem: 'frontmatter description must be a non-empty string' }, oddity)
|
|
238
|
+
}
|
|
239
|
+
if (whenToUse !== undefined && typeof whenToUse !== 'string') {
|
|
240
|
+
return withOddity({ problem: 'frontmatter whenToUse must be a string when present' }, oddity)
|
|
241
|
+
}
|
|
242
|
+
const booleans = booleanFields(record, 'frontmatter')
|
|
243
|
+
if (booleans.problem !== undefined) return withOddity({ problem: booleans.problem }, oddity)
|
|
244
|
+
return withOddity({
|
|
245
|
+
fields: {
|
|
246
|
+
name: effectiveName,
|
|
247
|
+
description: description.trim(),
|
|
248
|
+
...whenToUse === undefined ? {} : { whenToUse: whenToUse.trim() },
|
|
249
|
+
keepCodingInstructions: booleans.keepCodingInstructions,
|
|
250
|
+
force: booleans.force,
|
|
251
|
+
},
|
|
252
|
+
}, oddity)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Read the two shared boolean flags, defaulting each to false. */
|
|
256
|
+
function booleanFields(record: Record<string, unknown>, source: string): { keepCodingInstructions: boolean; force: boolean; problem?: string } {
|
|
257
|
+
const keep = record['keep-coding-instructions']
|
|
258
|
+
if (keep !== undefined && typeof keep !== 'boolean') {
|
|
259
|
+
return { keepCodingInstructions: false, force: false, problem: `${source} keep-coding-instructions must be a boolean when present` }
|
|
260
|
+
}
|
|
261
|
+
const force = record['force']
|
|
262
|
+
if (force !== undefined && typeof force !== 'boolean') {
|
|
263
|
+
return { keepCodingInstructions: false, force: false, problem: `${source} force must be a boolean when present` }
|
|
264
|
+
}
|
|
265
|
+
return { keepCodingInstructions: keep ?? false, force: force ?? false }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Parse a Claude Code `outputStyles` JSON file: one entry or an array of
|
|
270
|
+
* entries (the legacy `settings.json` collection form). Bad entries are
|
|
271
|
+
* skipped with one warning each; a bad file skips the whole file.
|
|
272
|
+
*/
|
|
273
|
+
function parseJsonFile(file: string, source: string | undefined, warn: Warn): OutputStyle[] {
|
|
274
|
+
if (source === undefined) return []
|
|
275
|
+
let raw: unknown
|
|
276
|
+
try {
|
|
277
|
+
raw = JSON.parse(source)
|
|
278
|
+
} catch (cause) {
|
|
279
|
+
warn(`skipping ${file}: invalid JSON (${cause instanceof Error ? cause.message : String(cause)})`)
|
|
280
|
+
return []
|
|
281
|
+
}
|
|
282
|
+
const records = Array.isArray(raw) ? raw : [raw]
|
|
283
|
+
const styles: OutputStyle[] = []
|
|
284
|
+
for (const [index, record] of records.entries()) {
|
|
285
|
+
const label = Array.isArray(raw) ? `${file}#${index + 1}` : file
|
|
286
|
+
const entry = parseJsonEntry(label, record)
|
|
287
|
+
if (entry.oddity !== undefined) warn(entry.oddity)
|
|
288
|
+
if (entry.problem !== undefined) {
|
|
289
|
+
warn(`skipping ${label}: ${entry.problem}`)
|
|
290
|
+
continue
|
|
291
|
+
}
|
|
292
|
+
if (entry.style !== undefined) styles.push(entry.style)
|
|
293
|
+
}
|
|
294
|
+
return styles
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Parse one Claude Code `outputStyles` JSON entry. */
|
|
298
|
+
function parseJsonEntry(label: string, record: unknown): { style?: OutputStyle; problem?: string; oddity?: string } {
|
|
299
|
+
if (record === null || typeof record !== 'object' || Array.isArray(record)) {
|
|
300
|
+
return { problem: 'must be a JSON object' }
|
|
301
|
+
}
|
|
302
|
+
const raw = record as Record<string, unknown>
|
|
303
|
+
const { name, description, prompt, whenToUse } = raw
|
|
304
|
+
if (typeof name !== 'string' || !isValidStyleName(name)) {
|
|
305
|
+
return { problem: 'name must be letters, digits, spaces, or hyphens, with at least one letter or digit and no leading/trailing space' }
|
|
306
|
+
}
|
|
307
|
+
if (typeof description !== 'string' || description.trim() === '') {
|
|
308
|
+
return { problem: 'description must be a non-empty string' }
|
|
309
|
+
}
|
|
310
|
+
if (typeof prompt !== 'string' || prompt.trim() === '') {
|
|
311
|
+
return { problem: 'prompt must be a non-empty string' }
|
|
312
|
+
}
|
|
313
|
+
const oddityKeys = Object.keys(raw).filter(key => !FRONTMATTER_KEYS.has(key) && key !== 'prompt')
|
|
314
|
+
const oddity = oddityKeys.length > 0 ? `${label}: ignoring unknown JSON field${oddityKeys.length > 1 ? 's' : ''} ${oddityKeys.join(', ')}` : undefined
|
|
315
|
+
if (whenToUse !== undefined && typeof whenToUse !== 'string') {
|
|
316
|
+
return withOddity({ problem: 'whenToUse must be a string when present' }, oddity)
|
|
317
|
+
}
|
|
318
|
+
const booleans = booleanFields(raw, 'JSON')
|
|
319
|
+
if (booleans.problem !== undefined) return withOddity({ problem: booleans.problem }, oddity)
|
|
320
|
+
return withOddity({
|
|
321
|
+
style: {
|
|
322
|
+
name,
|
|
323
|
+
description: description.trim(),
|
|
324
|
+
...whenToUse === undefined ? {} : { whenToUse: whenToUse.trim() },
|
|
325
|
+
body: prompt.trim(),
|
|
326
|
+
file: label,
|
|
327
|
+
format: 'json',
|
|
328
|
+
keepCodingInstructions: booleans.keepCodingInstructions,
|
|
329
|
+
force: booleans.force,
|
|
330
|
+
},
|
|
331
|
+
}, oddity)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Apply the style-body budget: bodies at most `maxChars` code points pass
|
|
336
|
+
* through; longer bodies are cut at the budget and closed with `marker` (the
|
|
337
|
+
* marker itself is not counted against the budget). The cut is code-point
|
|
338
|
+
* safe, so a multi-unit emoji is never split in half.
|
|
339
|
+
* @param body - the raw style body.
|
|
340
|
+
* @param maxChars - budget in code points; at least 1.
|
|
341
|
+
* @param marker - text appended at the truncation point.
|
|
342
|
+
* @returns the body as it will be injected.
|
|
343
|
+
*/
|
|
344
|
+
export function truncateStyle(body: string, maxChars: number, marker: string): string {
|
|
345
|
+
const chars = Array.from(body)
|
|
346
|
+
if (chars.length <= maxChars) return body
|
|
347
|
+
return chars.slice(0, maxChars).join('') + marker
|
|
348
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable domain vocabulary and type-table merges owned by this package.
|
|
3
|
+
*
|
|
4
|
+
* The selection domain is the single source of the per-session style choice;
|
|
5
|
+
* its record schema doubles as the durable validation boundary. The `style`
|
|
6
|
+
* projection key is declared here (its one home) and re-exported from the
|
|
7
|
+
* package root so consumers receive the `SessionProjectionMap` merge.
|
|
8
|
+
* @module dsh-output-styles/types
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { SessionId } from '@deepseek-ai/dsh-session'
|
|
12
|
+
import { z as zod } from 'zod'
|
|
13
|
+
import { defineDomain, domainTable } from '@deepseek-ai/dsh-storage-domain'
|
|
14
|
+
|
|
15
|
+
/** The reserved switch target that removes a session's selection. */
|
|
16
|
+
export const OFF = 'off'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Provenance marker stored with every selection record. It states who wrote
|
|
20
|
+
* the record, so a session's style choice can be attributed to this plugin
|
|
21
|
+
* when the log is rebuilt or audited.
|
|
22
|
+
*/
|
|
23
|
+
export const STYLE_SOURCE = { kind: 'plugin', plugin: 'dsh-output-styles' } as const
|
|
24
|
+
|
|
25
|
+
/** One durable per-session selection record. */
|
|
26
|
+
export const styleSelectionSchema = zod.object({
|
|
27
|
+
/** Selected style name; a name present in the style library at write time. */
|
|
28
|
+
style: zod.string().min(1),
|
|
29
|
+
/** Producer marker; always this plugin's own {@link STYLE_SOURCE}. */
|
|
30
|
+
source: zod.object({
|
|
31
|
+
kind: zod.literal('plugin'),
|
|
32
|
+
plugin: zod.string().min(1),
|
|
33
|
+
}),
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
/** Durable per-session selection record value. */
|
|
37
|
+
export interface StyleSelection extends zod.infer<typeof styleSelectionSchema> {}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The plugin's storage domain: one `selection` record per session, keyed by
|
|
41
|
+
* the session id. Versioned independently from the session log format.
|
|
42
|
+
*/
|
|
43
|
+
export const OUTPUT_STYLE_DOMAIN = defineDomain({
|
|
44
|
+
name: 'output_style',
|
|
45
|
+
version: 1,
|
|
46
|
+
tables: {
|
|
47
|
+
selection: domainTable<SessionId, StyleSelection>(styleSelectionSchema),
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
/** One option a client renders for the `style` projection. */
|
|
52
|
+
export interface StyleOption {
|
|
53
|
+
/** Switch target accepted by `/style`. */
|
|
54
|
+
value: string
|
|
55
|
+
/** Human-readable style name; the style's own name when no label exists. */
|
|
56
|
+
name: string
|
|
57
|
+
/** One user-facing sentence on what the style does. */
|
|
58
|
+
description: string
|
|
59
|
+
/** Optional guidance on when the style is useful; shown in pickers and listings. */
|
|
60
|
+
whenToUse?: string | undefined
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Whole wire value of the `style` session projection. */
|
|
64
|
+
export interface StyleSelectionView {
|
|
65
|
+
/** Every switchable style, in library order. */
|
|
66
|
+
options: StyleOption[]
|
|
67
|
+
/** Current selection, or null when the session has none. */
|
|
68
|
+
currentValue: string | null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Validates the `style` projection's wire payload before it leaves the host. */
|
|
72
|
+
export const styleSelectionViewSchema = zod.object({
|
|
73
|
+
options: zod.array(zod.object({
|
|
74
|
+
value: zod.string().min(1),
|
|
75
|
+
name: zod.string().min(1),
|
|
76
|
+
description: zod.string().min(1),
|
|
77
|
+
whenToUse: zod.string().min(1).optional(),
|
|
78
|
+
})),
|
|
79
|
+
currentValue: zod.string().min(1).nullable(),
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
declare module '@deepseek-ai/dsh-session-projection/types' {
|
|
83
|
+
interface SessionProjectionMap {
|
|
84
|
+
style: StyleSelectionView
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: concise
|
|
3
|
+
description: Terse, direct answers — minimal prose, no preamble.
|
|
4
|
+
whenToUse: Daily coding work, tool-heavy sessions, or when prompt length matters.
|
|
5
|
+
keep-coding-instructions: true
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are in the concise output style for this conversation.
|
|
9
|
+
|
|
10
|
+
- Lead with the direct answer; skip preamble, restatements, and filler.
|
|
11
|
+
- Prefer short paragraphs and bullets; cut explanations that are obvious from the code.
|
|
12
|
+
- 回答语言跟随用户语言:中文提问用中文回答,英文提问用英文回答。
|
|
13
|
+
- 只陈述结论与必要理由;不确定之处用一句话说明,不要展开。
|
|
14
|
+
- Code and commands over prose when they carry the information.
|
|
15
|
+
- End when the task is done; do not add closing summaries unless asked.
|
|
16
|
+
|
|
17
|
+
保持简洁:直接给出答案、必要代码与最短说明;不要开场白、客套、重复总结或多余的过渡句。
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: explanatory
|
|
3
|
+
description: Educational answers with short "Insights" that teach as you work.
|
|
4
|
+
whenToUse: Learning a codebase, onboarding, or when you want the reasoning behind each change.
|
|
5
|
+
keep-coding-instructions: true
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are in the explanatory output style for this conversation.
|
|
9
|
+
|
|
10
|
+
- Before or after each significant action, add a short insight that explains the underlying mechanism or design decision in plain language.
|
|
11
|
+
- Frame insights as the why behind the what: why this approach, why this order, why this edge case matters.
|
|
12
|
+
- Keep insights self-contained — a reader skipping them must still get a complete answer.
|
|
13
|
+
- 回答语言跟随用户语言:中文提问用中文回答,英文提问用英文回答。
|
|
14
|
+
- Do not let the insights lengthen the core work; the answer itself stays as terse as it would be without them.
|
package/styles/formal.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: formal
|
|
3
|
+
description: Formal, precise prose with complete sentences and defined terms.
|
|
4
|
+
whenToUse: Reports, documentation, release notes, or audiences expecting a formal register.
|
|
5
|
+
keep-coding-instructions: true
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are in the formal output style for this conversation.
|
|
9
|
+
|
|
10
|
+
- Write complete, well-formed sentences; avoid fragments, bullet-style shortcuts, and casual filler.
|
|
11
|
+
- Define terms on first use and prefer precise vocabulary over colloquial phrasing.
|
|
12
|
+
- Structure the response with clear headings and a short conclusion.
|
|
13
|
+
- 回答语言跟随用户语言:中文提问用中文回答,英文提问用英文回答。
|
|
14
|
+
- Stay neutral in tone; state assumptions and limitations explicitly when they affect the conclusion.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: step-by-step
|
|
3
|
+
description: Numbered reasoning steps with explicit intermediate results.
|
|
4
|
+
whenToUse: Debugging, design decisions, or when the user asks to see the reasoning.
|
|
5
|
+
keep-coding-instructions: true
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are in the step-by-step output style for this conversation.
|
|
9
|
+
|
|
10
|
+
- Break the work into numbered steps; state the goal of each step before executing it.
|
|
11
|
+
- Show intermediate results and how each step feeds the next.
|
|
12
|
+
- 回答语言跟随用户语言:中文提问用中文回答,英文提问用英文回答。
|
|
13
|
+
- When a step fails or an assumption breaks, say so and replan explicitly.
|
|
14
|
+
- Close with a final numbered step summarizing the outcome.
|
|
15
|
+
|
|
16
|
+
按步骤工作:每个步骤先写目标、再执行、再给出中间结果;假设被推翻或某步失败时显式说明并调整计划;最后一步总结结果。
|