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,442 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
|
|
5
|
+
import { parse } from "yaml";
|
|
6
|
+
//#region src/types.ts
|
|
7
|
+
/** The reserved switch target that removes a session's selection. */
|
|
8
|
+
const OFF = "off";
|
|
9
|
+
/**
|
|
10
|
+
* Provenance marker stored with every selection record. It states who wrote
|
|
11
|
+
* the record, so a session's style choice can be attributed to this plugin
|
|
12
|
+
* when the log is rebuilt or audited.
|
|
13
|
+
*/
|
|
14
|
+
const STYLE_SOURCE = {
|
|
15
|
+
kind: "plugin",
|
|
16
|
+
plugin: "dsh-output-styles"
|
|
17
|
+
};
|
|
18
|
+
/** One durable per-session selection record. */
|
|
19
|
+
const styleSelectionSchema = z.object({
|
|
20
|
+
/** Selected style name; a name present in the style library at write time. */
|
|
21
|
+
style: z.string().min(1),
|
|
22
|
+
/** Producer marker; always this plugin's own {@link STYLE_SOURCE}. */
|
|
23
|
+
source: z.object({
|
|
24
|
+
kind: z.literal("plugin"),
|
|
25
|
+
plugin: z.string().min(1)
|
|
26
|
+
})
|
|
27
|
+
});
|
|
28
|
+
/**
|
|
29
|
+
* The plugin's storage domain: one `selection` record per session, keyed by
|
|
30
|
+
* the session id. Versioned independently from the session log format.
|
|
31
|
+
*/
|
|
32
|
+
const OUTPUT_STYLE_DOMAIN = defineDomain({
|
|
33
|
+
name: "output_style",
|
|
34
|
+
version: 1,
|
|
35
|
+
tables: { selection: domainTable(styleSelectionSchema) }
|
|
36
|
+
});
|
|
37
|
+
/** Validates the `style` projection's wire payload before it leaves the host. */
|
|
38
|
+
const styleSelectionViewSchema = z.object({
|
|
39
|
+
options: z.array(z.object({
|
|
40
|
+
value: z.string().min(1),
|
|
41
|
+
name: z.string().min(1),
|
|
42
|
+
description: z.string().min(1),
|
|
43
|
+
whenToUse: z.string().min(1).optional()
|
|
44
|
+
})),
|
|
45
|
+
currentValue: z.string().min(1).nullable()
|
|
46
|
+
});
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/style-command.ts
|
|
49
|
+
/** Command name registered on `ctx.commands`; also the log's `command/run` name. */
|
|
50
|
+
const STYLE_COMMAND = "style";
|
|
51
|
+
/**
|
|
52
|
+
* Parse the text after `/style` into one switch decision. The empty string
|
|
53
|
+
* is `none` — the handler treats it as the listing form, and the projection
|
|
54
|
+
* fold ignores it. Anything else is a switch: `off` restores the default,
|
|
55
|
+
* and every other input is one style name taken verbatim (style names may
|
|
56
|
+
* contain spaces, so the whole remainder is the candidate). Unknown names
|
|
57
|
+
* are rejected by the handler against the library, and the fold commits only
|
|
58
|
+
* what the handler reports as successful, so both sides agree on what
|
|
59
|
+
* actually switched.
|
|
60
|
+
* @param rawInput - verbatim text after the command name.
|
|
61
|
+
* @returns the decision; non-empty inputs other than `off` name a style.
|
|
62
|
+
*/
|
|
63
|
+
function parseStyleInput(rawInput) {
|
|
64
|
+
const arg = rawInput.trim();
|
|
65
|
+
if (arg === "") return { kind: "none" };
|
|
66
|
+
if (arg === "off") return { kind: "off" };
|
|
67
|
+
return {
|
|
68
|
+
kind: "switch",
|
|
69
|
+
name: arg
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** State for the empty log. */
|
|
73
|
+
const EMPTY_STYLE_STATE = {
|
|
74
|
+
current: null,
|
|
75
|
+
pending: null
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* One-event transition of the `style` projection unit. Only a successful
|
|
79
|
+
* `/style` command settles into `current`: `command/run` parks the target as
|
|
80
|
+
* `pending` and its paired `command/done` commits it on `kind: 'success'` or
|
|
81
|
+
* drops it otherwise. Every other event returns the same reference (the
|
|
82
|
+
* registry's change gate).
|
|
83
|
+
* @param state - the folded state before `event`.
|
|
84
|
+
* @param event - one committed session event.
|
|
85
|
+
* @returns the next state; the same reference when the event leaves it unchanged.
|
|
86
|
+
*/
|
|
87
|
+
function applyStyleEvent(state, event) {
|
|
88
|
+
if (event.type === "command/run") {
|
|
89
|
+
if (event.data.name !== "style" || event.data.args === void 0) return state;
|
|
90
|
+
const input = parseStyleInput(event.data.args);
|
|
91
|
+
if (input.kind === "none") return state;
|
|
92
|
+
const commandId = String(event.data.commandId);
|
|
93
|
+
if (state.pending?.commandId === commandId) return state;
|
|
94
|
+
const target = input.kind === "off" ? { off: true } : { name: input.name };
|
|
95
|
+
return {
|
|
96
|
+
...state,
|
|
97
|
+
pending: {
|
|
98
|
+
commandId,
|
|
99
|
+
target
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (event.type !== "command/done" || state.pending === null) return state;
|
|
104
|
+
if (state.pending.commandId !== String(event.data.commandId)) return state;
|
|
105
|
+
if (event.data.kind !== "success") return {
|
|
106
|
+
current: state.current,
|
|
107
|
+
pending: null
|
|
108
|
+
};
|
|
109
|
+
return {
|
|
110
|
+
current: "off" in state.pending.target ? null : state.pending.target.name,
|
|
111
|
+
pending: null
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/style-library.ts
|
|
116
|
+
/**
|
|
117
|
+
* Style-library loading: one style per `*.md` file (frontmatter + body),
|
|
118
|
+
* with optional Claude Code `outputStyles` JSON compatibility (single entry
|
|
119
|
+
* or an array of entries per file).
|
|
120
|
+
*
|
|
121
|
+
* A style file that does not parse is skipped with a warning; the plugin
|
|
122
|
+
* stays loadable. Structural ambiguity — a duplicate style name, a style
|
|
123
|
+
* named `off`, or two styles declaring `force` — fails the load because it
|
|
124
|
+
* would silently change which body gets injected.
|
|
125
|
+
* @module dsh-output-styles/style-library
|
|
126
|
+
*/
|
|
127
|
+
/** Character set for style names: letters, digits, spaces, and hyphens only. */
|
|
128
|
+
const STYLE_NAME_RE = /^[\p{L}\p{N} -]+$/u;
|
|
129
|
+
/**
|
|
130
|
+
* Whether a name is a legal style name and switch target: at least one
|
|
131
|
+
* letter or digit, only letters/digits/spaces/hyphens, and no leading or
|
|
132
|
+
* trailing space. Spaces are the only whitespace allowed, so a name is
|
|
133
|
+
* always a single switchable line the model can echo back. `off` passes this
|
|
134
|
+
* check but is a reserved target rejected by the library.
|
|
135
|
+
* @param name - candidate style name.
|
|
136
|
+
* @returns whether the name is legal.
|
|
137
|
+
*/
|
|
138
|
+
function isValidStyleName(name) {
|
|
139
|
+
if (name === "" || name !== name.trim()) return false;
|
|
140
|
+
if (!STYLE_NAME_RE.test(name)) return false;
|
|
141
|
+
return /[\p{L}\p{N}]/u.test(name);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Load the style library from one or more directories. Later directories
|
|
145
|
+
* override earlier ones on a same-named style (the Claude Code
|
|
146
|
+
* "closest-to-the-working-directory wins" rule); duplicates within one
|
|
147
|
+
* directory still fail the load. Deterministic order: directories in the
|
|
148
|
+
* given order, files within a directory sorted by code unit.
|
|
149
|
+
* @param stylesDirs - absolute directories, lowest priority first.
|
|
150
|
+
* @param options.compatJson - whether `*.json` entries are loaded.
|
|
151
|
+
* @param warn - warning sink (skipped files, unknown frontmatter keys).
|
|
152
|
+
* @returns the library keyed by style name, in directory/file order.
|
|
153
|
+
* @throws when a directory is unreadable, a style is named `off`, two files
|
|
154
|
+
* in one directory declare the same name, or two styles declare `force`.
|
|
155
|
+
*/
|
|
156
|
+
function loadStyleLibrary(stylesDirs, options, warn) {
|
|
157
|
+
const styles = /* @__PURE__ */ new Map();
|
|
158
|
+
for (const dir of stylesDirs) for (const [name, style] of loadStyleDir(dir, options, warn)) styles.set(name, style);
|
|
159
|
+
const forced = [...styles.values()].filter((style) => style.force);
|
|
160
|
+
if (forced.length > 1) throw new Error(`dsh-output-styles: styles ${forced.map((style) => style.file).join(" and ")} both declare force; at most one style may be forced`);
|
|
161
|
+
return styles;
|
|
162
|
+
}
|
|
163
|
+
/** Load every style in one directory (duplicates within it fail the load). */
|
|
164
|
+
function loadStyleDir(stylesDir, options, warn) {
|
|
165
|
+
let entries;
|
|
166
|
+
try {
|
|
167
|
+
entries = readdirSync(stylesDir, { withFileTypes: true });
|
|
168
|
+
} catch (cause) {
|
|
169
|
+
throw new Error(`dsh-output-styles: style directory ${stylesDir} is unreadable`, { cause });
|
|
170
|
+
}
|
|
171
|
+
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
172
|
+
const styles = /* @__PURE__ */ new Map();
|
|
173
|
+
const addStyle = (style) => {
|
|
174
|
+
if (style.name === "off") throw new Error(`dsh-output-styles: style ${style.file} is named "off", which is reserved for switching output styles off`);
|
|
175
|
+
if (styles.has(style.name)) throw new Error(`dsh-output-styles: duplicate style name "${style.name}" (${styles.get(style.name)?.file} and ${style.file})`);
|
|
176
|
+
styles.set(style.name, style);
|
|
177
|
+
};
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
if (!entry.isFile()) continue;
|
|
180
|
+
const file = entry.name;
|
|
181
|
+
if (file.endsWith(".md")) {
|
|
182
|
+
const parsed = parseMarkdownStyle(file, readStyleFile(stylesDir, file, warn));
|
|
183
|
+
if (parsed.oddity !== void 0) warn(parsed.oddity);
|
|
184
|
+
if (parsed.problem !== void 0) {
|
|
185
|
+
warn(`skipping ${file}: ${parsed.problem}`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (parsed.style !== void 0) addStyle(parsed.style);
|
|
189
|
+
} else if (file.endsWith(".json")) {
|
|
190
|
+
if (!options.compatJson) {
|
|
191
|
+
warn(`ignoring ${file}: JSON style loading is disabled (compatJson: false)`);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
for (const style of parseJsonFile(file, readStyleFile(stylesDir, file, warn), warn)) addStyle(style);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return styles;
|
|
198
|
+
}
|
|
199
|
+
/** Read one library file; an unreadable file is a skipped-file warning. */
|
|
200
|
+
function readStyleFile(stylesDir, file, warn) {
|
|
201
|
+
try {
|
|
202
|
+
return readFileSync(join(stylesDir, file), "utf8");
|
|
203
|
+
} catch (cause) {
|
|
204
|
+
warn(`skipping ${file}: unreadable (${cause instanceof Error ? cause.message : String(cause)})`);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/** Parse a `---`-fenced frontmatter style file. */
|
|
209
|
+
function parseMarkdownStyle(file, source) {
|
|
210
|
+
if (source === void 0) return {};
|
|
211
|
+
const open = /^---[ \t]*\r?\n/.exec(source);
|
|
212
|
+
if (open === null) return { problem: "missing `---` frontmatter block" };
|
|
213
|
+
const close = /\r?\n---[ \t]*(?:\r?\n|$)/.exec(source.slice(open[0].length));
|
|
214
|
+
if (close === null) return { problem: "unterminated `---` frontmatter block" };
|
|
215
|
+
const frontmatter = source.slice(open[0].length, open[0].length + close.index);
|
|
216
|
+
const bodyStart = open[0].length + close.index + close[0].length;
|
|
217
|
+
const body = source.slice(bodyStart).trim();
|
|
218
|
+
const parsed = parseFrontmatter(file, frontmatter);
|
|
219
|
+
if (parsed.problem !== void 0) return { problem: parsed.problem };
|
|
220
|
+
if (body === "") return { problem: "style body must be non-empty" };
|
|
221
|
+
const fields = parsed.fields;
|
|
222
|
+
if (fields === void 0) return {};
|
|
223
|
+
const style = {
|
|
224
|
+
name: fields.name,
|
|
225
|
+
description: fields.description,
|
|
226
|
+
...fields.whenToUse === void 0 ? {} : { whenToUse: fields.whenToUse },
|
|
227
|
+
body,
|
|
228
|
+
file,
|
|
229
|
+
format: "md",
|
|
230
|
+
keepCodingInstructions: fields.keepCodingInstructions,
|
|
231
|
+
force: fields.force
|
|
232
|
+
};
|
|
233
|
+
return parsed.oddity === void 0 ? { style } : {
|
|
234
|
+
style,
|
|
235
|
+
oddity: parsed.oddity
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
/** Attach a present oddity without an explicit-undefined optional key. */
|
|
239
|
+
function withOddity(value, oddity) {
|
|
240
|
+
return oddity === void 0 ? value : {
|
|
241
|
+
...value,
|
|
242
|
+
oddity
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/** The file name a `name`-less style inherits: the file name without its extension. */
|
|
246
|
+
function defaultStyleName(file) {
|
|
247
|
+
return file.slice(0, file.length - 3);
|
|
248
|
+
}
|
|
249
|
+
/** Keys both frontmatter formats accept, plus the booleans they share. */
|
|
250
|
+
const FRONTMATTER_KEYS = /* @__PURE__ */ new Set([
|
|
251
|
+
"name",
|
|
252
|
+
"description",
|
|
253
|
+
"whenToUse",
|
|
254
|
+
"keep-coding-instructions",
|
|
255
|
+
"force"
|
|
256
|
+
]);
|
|
257
|
+
/** Validate one frontmatter block into {@link StyleFields}. */
|
|
258
|
+
function parseFrontmatter(file, frontmatter) {
|
|
259
|
+
let raw;
|
|
260
|
+
try {
|
|
261
|
+
raw = parse(frontmatter);
|
|
262
|
+
} catch (cause) {
|
|
263
|
+
return { problem: `frontmatter is not valid YAML (${cause instanceof Error ? cause.message : String(cause)})` };
|
|
264
|
+
}
|
|
265
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return { problem: "frontmatter must be a mapping of scalar fields" };
|
|
266
|
+
const record = raw;
|
|
267
|
+
const oddityKeys = Object.keys(record).filter((key) => !FRONTMATTER_KEYS.has(key));
|
|
268
|
+
const oddity = oddityKeys.length > 0 ? `${file}: ignoring unknown frontmatter field${oddityKeys.length > 1 ? "s" : ""} ${oddityKeys.join(", ")}` : void 0;
|
|
269
|
+
const { name, description, whenToUse } = record;
|
|
270
|
+
const effectiveName = name === void 0 ? defaultStyleName(file) : name;
|
|
271
|
+
if (typeof effectiveName !== "string" || !isValidStyleName(effectiveName)) 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);
|
|
272
|
+
if (typeof description !== "string" || description.trim() === "") return withOddity({ problem: "frontmatter description must be a non-empty string" }, oddity);
|
|
273
|
+
if (whenToUse !== void 0 && typeof whenToUse !== "string") return withOddity({ problem: "frontmatter whenToUse must be a string when present" }, oddity);
|
|
274
|
+
const booleans = booleanFields(record, "frontmatter");
|
|
275
|
+
if (booleans.problem !== void 0) return withOddity({ problem: booleans.problem }, oddity);
|
|
276
|
+
return withOddity({ fields: {
|
|
277
|
+
name: effectiveName,
|
|
278
|
+
description: description.trim(),
|
|
279
|
+
...whenToUse === void 0 ? {} : { whenToUse: whenToUse.trim() },
|
|
280
|
+
keepCodingInstructions: booleans.keepCodingInstructions,
|
|
281
|
+
force: booleans.force
|
|
282
|
+
} }, oddity);
|
|
283
|
+
}
|
|
284
|
+
/** Read the two shared boolean flags, defaulting each to false. */
|
|
285
|
+
function booleanFields(record, source) {
|
|
286
|
+
const keep = record["keep-coding-instructions"];
|
|
287
|
+
if (keep !== void 0 && typeof keep !== "boolean") return {
|
|
288
|
+
keepCodingInstructions: false,
|
|
289
|
+
force: false,
|
|
290
|
+
problem: `${source} keep-coding-instructions must be a boolean when present`
|
|
291
|
+
};
|
|
292
|
+
const force = record["force"];
|
|
293
|
+
if (force !== void 0 && typeof force !== "boolean") return {
|
|
294
|
+
keepCodingInstructions: false,
|
|
295
|
+
force: false,
|
|
296
|
+
problem: `${source} force must be a boolean when present`
|
|
297
|
+
};
|
|
298
|
+
return {
|
|
299
|
+
keepCodingInstructions: keep ?? false,
|
|
300
|
+
force: force ?? false
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Parse a Claude Code `outputStyles` JSON file: one entry or an array of
|
|
305
|
+
* entries (the legacy `settings.json` collection form). Bad entries are
|
|
306
|
+
* skipped with one warning each; a bad file skips the whole file.
|
|
307
|
+
*/
|
|
308
|
+
function parseJsonFile(file, source, warn) {
|
|
309
|
+
if (source === void 0) return [];
|
|
310
|
+
let raw;
|
|
311
|
+
try {
|
|
312
|
+
raw = JSON.parse(source);
|
|
313
|
+
} catch (cause) {
|
|
314
|
+
warn(`skipping ${file}: invalid JSON (${cause instanceof Error ? cause.message : String(cause)})`);
|
|
315
|
+
return [];
|
|
316
|
+
}
|
|
317
|
+
const records = Array.isArray(raw) ? raw : [raw];
|
|
318
|
+
const styles = [];
|
|
319
|
+
for (const [index, record] of records.entries()) {
|
|
320
|
+
const label = Array.isArray(raw) ? `${file}#${index + 1}` : file;
|
|
321
|
+
const entry = parseJsonEntry(label, record);
|
|
322
|
+
if (entry.oddity !== void 0) warn(entry.oddity);
|
|
323
|
+
if (entry.problem !== void 0) {
|
|
324
|
+
warn(`skipping ${label}: ${entry.problem}`);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (entry.style !== void 0) styles.push(entry.style);
|
|
328
|
+
}
|
|
329
|
+
return styles;
|
|
330
|
+
}
|
|
331
|
+
/** Parse one Claude Code `outputStyles` JSON entry. */
|
|
332
|
+
function parseJsonEntry(label, record) {
|
|
333
|
+
if (record === null || typeof record !== "object" || Array.isArray(record)) return { problem: "must be a JSON object" };
|
|
334
|
+
const raw = record;
|
|
335
|
+
const { name, description, prompt, whenToUse } = raw;
|
|
336
|
+
if (typeof name !== "string" || !isValidStyleName(name)) return { problem: "name must be letters, digits, spaces, or hyphens, with at least one letter or digit and no leading/trailing space" };
|
|
337
|
+
if (typeof description !== "string" || description.trim() === "") return { problem: "description must be a non-empty string" };
|
|
338
|
+
if (typeof prompt !== "string" || prompt.trim() === "") return { problem: "prompt must be a non-empty string" };
|
|
339
|
+
const oddityKeys = Object.keys(raw).filter((key) => !FRONTMATTER_KEYS.has(key) && key !== "prompt");
|
|
340
|
+
const oddity = oddityKeys.length > 0 ? `${label}: ignoring unknown JSON field${oddityKeys.length > 1 ? "s" : ""} ${oddityKeys.join(", ")}` : void 0;
|
|
341
|
+
if (whenToUse !== void 0 && typeof whenToUse !== "string") return withOddity({ problem: "whenToUse must be a string when present" }, oddity);
|
|
342
|
+
const booleans = booleanFields(raw, "JSON");
|
|
343
|
+
if (booleans.problem !== void 0) return withOddity({ problem: booleans.problem }, oddity);
|
|
344
|
+
return withOddity({ style: {
|
|
345
|
+
name,
|
|
346
|
+
description: description.trim(),
|
|
347
|
+
...whenToUse === void 0 ? {} : { whenToUse: whenToUse.trim() },
|
|
348
|
+
body: prompt.trim(),
|
|
349
|
+
file: label,
|
|
350
|
+
format: "json",
|
|
351
|
+
keepCodingInstructions: booleans.keepCodingInstructions,
|
|
352
|
+
force: booleans.force
|
|
353
|
+
} }, oddity);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Apply the style-body budget: bodies at most `maxChars` code points pass
|
|
357
|
+
* through; longer bodies are cut at the budget and closed with `marker` (the
|
|
358
|
+
* marker itself is not counted against the budget). The cut is code-point
|
|
359
|
+
* safe, so a multi-unit emoji is never split in half.
|
|
360
|
+
* @param body - the raw style body.
|
|
361
|
+
* @param maxChars - budget in code points; at least 1.
|
|
362
|
+
* @param marker - text appended at the truncation point.
|
|
363
|
+
* @returns the body as it will be injected.
|
|
364
|
+
*/
|
|
365
|
+
function truncateStyle(body, maxChars, marker) {
|
|
366
|
+
const chars = Array.from(body);
|
|
367
|
+
if (chars.length <= maxChars) return body;
|
|
368
|
+
return chars.slice(0, maxChars).join("") + marker;
|
|
369
|
+
}
|
|
370
|
+
//#endregion
|
|
371
|
+
//#region src/invariant.ts
|
|
372
|
+
/** Full npm package name owning the reported failures. */
|
|
373
|
+
const PACKAGE_NAME = "dsh-output-styles";
|
|
374
|
+
/** Cordis companion plugin name. */
|
|
375
|
+
const name = "dsh-output-styles-invariant";
|
|
376
|
+
/** Service required before the companion can reserve package ownership. */
|
|
377
|
+
const inject = ["invariants"];
|
|
378
|
+
/** Facts for the standalone companion: envelope checks only, no library/domain handle. */
|
|
379
|
+
const COMPANION_FACTS = { knownStyles: () => void 0 };
|
|
380
|
+
/**
|
|
381
|
+
* Build the installer over a facts source. The standalone companion and the
|
|
382
|
+
* main plugin share this body; only the facts differ.
|
|
383
|
+
* @param facts - library and domain access for the checks.
|
|
384
|
+
* @returns the installer the host registry activates in its child context.
|
|
385
|
+
*/
|
|
386
|
+
function installInvariant(facts) {
|
|
387
|
+
return (ctx, fail) => {
|
|
388
|
+
const pending = /* @__PURE__ */ new Map();
|
|
389
|
+
ctx.on("domain/changed", (change) => {
|
|
390
|
+
if (change.domain !== "output_style" || change.table !== "selection" || change.operation !== "put") return;
|
|
391
|
+
const value = change.value;
|
|
392
|
+
if (typeof value.style !== "string" || !isValidStyleName(value.style)) fail(`selection record names invalid style ${JSON.stringify(value.style)}`);
|
|
393
|
+
if (JSON.stringify(value.source) !== JSON.stringify(STYLE_SOURCE)) fail(`selection record source is ${JSON.stringify(value.source)}; expected this plugin's own marker`);
|
|
394
|
+
const known = facts.knownStyles();
|
|
395
|
+
if (known !== void 0 && typeof value.style === "string" && !known.has(value.style)) fail(`selection record names style "${value.style}" that is not in the style library`);
|
|
396
|
+
});
|
|
397
|
+
ctx.on("session/event", (session, event) => {
|
|
398
|
+
if (event.type === "command/run" && event.data.name === "style" && event.data.args !== void 0) {
|
|
399
|
+
const input = parseStyleInput(event.data.args);
|
|
400
|
+
if (input.kind === "off") pending.set(String(event.data.commandId), {
|
|
401
|
+
session,
|
|
402
|
+
expected: { off: true }
|
|
403
|
+
});
|
|
404
|
+
else if (input.kind === "switch") pending.set(String(event.data.commandId), {
|
|
405
|
+
session,
|
|
406
|
+
expected: { name: input.name }
|
|
407
|
+
});
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (event.type !== "command/done") return;
|
|
411
|
+
const entry = pending.get(String(event.data.commandId));
|
|
412
|
+
if (entry === void 0) return;
|
|
413
|
+
pending.delete(String(event.data.commandId));
|
|
414
|
+
if (event.data.kind !== "success" || facts.selectionFor === void 0) return;
|
|
415
|
+
const record = facts.selectionFor(entry.session.id);
|
|
416
|
+
if ("off" in entry.expected) {
|
|
417
|
+
if (record !== void 0) fail(`/style off settled on session "${entry.session.id}" but its selection record still exists`);
|
|
418
|
+
} else if (record === void 0 || record.style !== entry.expected.name) fail(`/style ${entry.expected.name} settled on session "${entry.session.id}" without a matching selection record`);
|
|
419
|
+
}, { global: true });
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Resolve the host registry through Cordis's named service lookup. Keeping
|
|
424
|
+
* this narrow local contract lets the companion build without host source
|
|
425
|
+
* files; a composed DSH profile still supplies the real `invariants` service.
|
|
426
|
+
* @param ctx - Cordis context carrying the host service.
|
|
427
|
+
* @returns the host invariant registry.
|
|
428
|
+
* @throws {Error} when the companion is loaded without its host service.
|
|
429
|
+
*/
|
|
430
|
+
function getInvariantRegistry(ctx) {
|
|
431
|
+
const registry = ctx.get("invariants");
|
|
432
|
+
if (registry === void 0) throw new Error(`invariant companion requires the "invariants" service for ${PACKAGE_NAME}`);
|
|
433
|
+
return registry;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Register the standalone companion with envelope-only facts.
|
|
437
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
438
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
439
|
+
*/
|
|
440
|
+
const apply = (ctx) => Promise.resolve(getInvariantRegistry(ctx).register(PACKAGE_NAME, installInvariant(COMPANION_FACTS)));
|
|
441
|
+
//#endregion
|
|
442
|
+
export { styleSelectionSchema as _, name as a, loadStyleLibrary as c, STYLE_COMMAND as d, applyStyleEvent as f, STYLE_SOURCE as g, OUTPUT_STYLE_DOMAIN as h, installInvariant as i, truncateStyle as l, OFF as m, apply as n, STYLE_NAME_RE as o, parseStyleInput as p, inject as r, isValidStyleName as s, PACKAGE_NAME as t, EMPTY_STYLE_STATE as u, styleSelectionViewSchema as v };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser half of `dsh-output-styles`: a popup picker decorating the HOST
|
|
3
|
+
* `/style` command. The picker reads the `style` session projection
|
|
4
|
+
* (`{ options, currentValue }`), which the host plugin keeps fresh, and
|
|
5
|
+
* submits the completed `/style <name>` / `/style off` line back through the
|
|
6
|
+
* command Remote — so every switch keeps the host's durable command
|
|
7
|
+
* lifecycle (`command/run`/`command/done`) and the projection stays the
|
|
8
|
+
* single displayed fact.
|
|
9
|
+
* @module dsh-output-styles/client
|
|
10
|
+
*/
|
|
11
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
|
|
12
|
+
import { type StyleKey } from './locales.ts';
|
|
13
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
14
|
+
interface LocaleNamespaceMap {
|
|
15
|
+
/** The style picker's copy. */
|
|
16
|
+
style: StyleKey;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Client plugin name; keep stable after publishing. */
|
|
20
|
+
export declare const name = "dsh-output-styles-client";
|
|
21
|
+
/** Required client services: the command surface, the sessions face, the command Remote, and locale. */
|
|
22
|
+
export declare const inject: string[];
|
|
23
|
+
/**
|
|
24
|
+
* Client plugin body: register the `style` dictionaries and decorate the
|
|
25
|
+
* host `/style` command's bare invocation with the projection-backed picker.
|
|
26
|
+
* @param ctx - client root context.
|
|
27
|
+
*/
|
|
28
|
+
export declare function apply(ctx: ClientContext): void;
|
|
29
|
+
export type { SessionId } from '@deepseek-ai/dsh-client-runtime/client';
|
|
30
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAa,MAAM,wCAAwC,CAAA;AAMtF,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAA;AAEpD,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,+BAA+B;QAC/B,KAAK,EAAE,QAAQ,CAAA;KAChB;CACF;AAED,wDAAwD;AACxD,eAAO,MAAM,IAAI,6BAA6B,CAAA;AAE9C,wGAAwG;AACxG,eAAO,MAAM,MAAM,UAAgD,CAAA;AA6BnE;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAqC9C;AAED,YAAY,EAAE,SAAS,EAAE,MAAM,wCAAwC,CAAA"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
2
|
+
export declare const zh: {
|
|
3
|
+
'option.off': string;
|
|
4
|
+
'option.offDetail': string;
|
|
5
|
+
};
|
|
6
|
+
/** The style picker namespace key union. */
|
|
7
|
+
export type StyleKey = keyof typeof zh;
|
|
8
|
+
/** English dictionary, checked complete against the zh key set. */
|
|
9
|
+
export declare const en: {
|
|
10
|
+
'option.off': string;
|
|
11
|
+
'option.offDetail': string;
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=locales.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;CAGmB,CAAA;AAElC,4CAA4C;AAC5C,MAAM,MAAM,QAAQ,GAAG,MAAM,OAAO,EAAE,CAAA;AAEtC,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;CAGqB,CAAA"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serializable configuration, schema, and direct-call defaults.
|
|
3
|
+
*
|
|
4
|
+
* Every tunable lives here: a deployment changes behavior through
|
|
5
|
+
* `cordis.yml`, never by editing source. The schema is validated by the
|
|
6
|
+
* harness Loader while the plugin loads; invalid configuration fails the
|
|
7
|
+
* load with an actionable error.
|
|
8
|
+
* @module dsh-output-styles/config
|
|
9
|
+
*/
|
|
10
|
+
import z from '@deepseek-ai/schemastery';
|
|
11
|
+
/** Plugin configuration supplied by the profile composition. */
|
|
12
|
+
export interface Config {
|
|
13
|
+
/**
|
|
14
|
+
* Directories holding the style library (`*.md`, and with {@link Config.compatJson}
|
|
15
|
+
* also `*.json`). Each entry resolves against the process working directory.
|
|
16
|
+
* Later directories override earlier ones on a same-named style; the bundled
|
|
17
|
+
* `styles/` directory participates as the lowest-priority entry unless
|
|
18
|
+
* {@link Config.includeBuiltins} is false. An empty list means the bundled
|
|
19
|
+
* library only (or none, with `includeBuiltins: false`). A bare string is
|
|
20
|
+
* accepted as a single-directory list.
|
|
21
|
+
*/
|
|
22
|
+
stylesDir?: string | string[];
|
|
23
|
+
/** Style-body budget in characters; longer bodies are truncated at the budget with a marker. */
|
|
24
|
+
maxStyleChars?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Style injected into sessions that never selected one (and no project
|
|
27
|
+
* settings default exists). The empty string (default) means new sessions
|
|
28
|
+
* get no style — the session's own selection, made through `/style`, is
|
|
29
|
+
* always what wins for a session that has one.
|
|
30
|
+
*/
|
|
31
|
+
defaultStyle?: string;
|
|
32
|
+
/** Load Claude Code `outputStyles` JSON entries (`{ name, description, prompt }`) beside Markdown styles. */
|
|
33
|
+
compatJson?: boolean;
|
|
34
|
+
/** Order of the injected system-prompt section (90: after the persona, before tool guidance at 100–199). */
|
|
35
|
+
sectionOrder?: number;
|
|
36
|
+
/** Marker appended at the truncation point when a style body exceeds {@link Config.maxStyleChars}. */
|
|
37
|
+
truncationMarker?: string;
|
|
38
|
+
/** Include the package's bundled `styles/` directory in the library. */
|
|
39
|
+
includeBuiltins?: boolean;
|
|
40
|
+
/** Reload the library when a style file changes on disk (default true). */
|
|
41
|
+
watchStyles?: boolean;
|
|
42
|
+
}
|
|
43
|
+
/** Configuration after defaults have been resolved. */
|
|
44
|
+
export interface ResolvedConfig {
|
|
45
|
+
/** Absolute style-library directories, lowest priority first (bundled styles first when included). */
|
|
46
|
+
stylesDirs: string[];
|
|
47
|
+
/** Style-body budget in characters; at least 1. */
|
|
48
|
+
maxStyleChars: number;
|
|
49
|
+
/** Style injected into sessions that never selected one; `''` means none. */
|
|
50
|
+
defaultStyle: string;
|
|
51
|
+
/** Whether Claude Code `outputStyles` JSON entries are loaded. */
|
|
52
|
+
compatJson: boolean;
|
|
53
|
+
/** Order of the injected system-prompt section; a finite number. */
|
|
54
|
+
sectionOrder: number;
|
|
55
|
+
/** Marker appended at the truncation point. */
|
|
56
|
+
truncationMarker: string;
|
|
57
|
+
/** Whether the bundled `styles/` directory participates. */
|
|
58
|
+
includeBuiltins: boolean;
|
|
59
|
+
/** Whether the library reloads on style-file changes. */
|
|
60
|
+
watchStyles: boolean;
|
|
61
|
+
}
|
|
62
|
+
/** Loader-visible configuration schema and defaults. */
|
|
63
|
+
export declare const Config: z<Config>;
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the same defaults for direct callers that bypass the Cordis Loader,
|
|
66
|
+
* and fail loud on values the schema cannot express (a non-finite section
|
|
67
|
+
* order).
|
|
68
|
+
* @param config - Partial serialized configuration.
|
|
69
|
+
* @param defaultStylesDir - Absolute bundled directory used when built-ins are included.
|
|
70
|
+
* @returns Configuration with every default applied.
|
|
71
|
+
*/
|
|
72
|
+
export declare function resolveConfig(config: Config, defaultStylesDir: string): ResolvedConfig;
|
|
73
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,CAAC,MAAM,0BAA0B,CAAA;AAGxC,gEAAgE;AAChE,MAAM,WAAW,MAAM;IACrB;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IAC7B,gGAAgG;IAChG,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,6GAA6G;IAC7G,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,4GAA4G;IAC5G,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,sGAAsG;IACtG,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,wEAAwE;IACxE,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,2EAA2E;IAC3E,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB;AAED,uDAAuD;AACvD,MAAM,WAAW,cAAc;IAC7B,sGAAsG;IACtG,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,mDAAmD;IACnD,aAAa,EAAE,MAAM,CAAA;IACrB,6EAA6E;IAC7E,YAAY,EAAE,MAAM,CAAA;IACpB,kEAAkE;IAClE,UAAU,EAAE,OAAO,CAAA;IACnB,oEAAoE;IACpE,YAAY,EAAE,MAAM,CAAA;IACpB,+CAA+C;IAC/C,gBAAgB,EAAE,MAAM,CAAA;IACxB,4DAA4D;IAC5D,eAAe,EAAE,OAAO,CAAA;IACxB,yDAAyD;IACzD,WAAW,EAAE,OAAO,CAAA;CACrB;AAED,wDAAwD;AACxD,eAAO,MAAM,MAAM,EAAE,CAAC,CAAC,MAAM,CAS3B,CAAA;AAEF;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,GAAG,cAAc,CAuBtF"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dsh-output-styles`: Claude Code `outputStyles`-equivalent runtime output
|
|
3
|
+
* styles for DeepSeek Harness. The plugin registers a model-visible system
|
|
4
|
+
* prompt section that injects the current session's style body, a `/style`
|
|
5
|
+
* slash command that switches it, per-session persistence over the
|
|
6
|
+
* `output_style` storage domain, and the `style` session projection.
|
|
7
|
+
* @module dsh-output-styles
|
|
8
|
+
*/
|
|
9
|
+
/** Cordis plugin name; keep this stable after publishing. */
|
|
10
|
+
export declare const name = "dsh-output-styles";
|
|
11
|
+
/**
|
|
12
|
+
* Services that must exist before the plugin applies: the prompt-assembly
|
|
13
|
+
* registry for the injected section, and the storage domain facility for
|
|
14
|
+
* per-session persistence. A composition without a routed kv backend keeps
|
|
15
|
+
* the plugin pending until the storage rows appear (Cordis dependency
|
|
16
|
+
* semantics), instead of racing a parallel mount.
|
|
17
|
+
*/
|
|
18
|
+
export declare const inject: string[];
|
|
19
|
+
export { Config, resolveConfig } from './config.ts';
|
|
20
|
+
export type { ResolvedConfig } from './config.ts';
|
|
21
|
+
export { apply, DEFAULT_STYLES_DIR, OutputStyleRuntime, STYLE_SECTION_NAME } from './runtime.ts';
|
|
22
|
+
export { applyStyleEvent, EMPTY_STYLE_STATE, parseStyleInput, STYLE_COMMAND, } from './style-command.ts';
|
|
23
|
+
export type { StyleFoldState, StyleInput } from './style-command.ts';
|
|
24
|
+
export { isValidStyleName, loadStyleLibrary, STYLE_NAME_RE, truncateStyle, } from './style-library.ts';
|
|
25
|
+
export type { OutputStyle } from './style-library.ts';
|
|
26
|
+
export { OFF, OUTPUT_STYLE_DOMAIN, STYLE_SOURCE, styleSelectionSchema, styleSelectionViewSchema, } from './types.ts';
|
|
27
|
+
export type { StyleOption, StyleSelection, StyleSelectionView } from './types.ts';
|
|
28
|
+
export { installInvariant, PACKAGE_NAME } from './invariant.ts';
|
|
29
|
+
export type { InvariantFacts, InvariantInstaller, InvariantRegistry } from './invariant.ts';
|
|
30
|
+
export type * from './types.ts';
|
|
31
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,6DAA6D;AAC7D,eAAO,MAAM,IAAI,sBAAsB,CAAA;AAEvC;;;;;;GAMG;AACH,eAAO,MAAM,MAAM,UAAoC,CAAA;AAEvD,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AACnD,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACjD,OAAO,EAAE,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAChG,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,aAAa,GACd,MAAM,oBAAoB,CAAA;AAC3B,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AACpE,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,EACb,aAAa,GACd,MAAM,oBAAoB,CAAA;AAC3B,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,EACL,GAAG,EACH,mBAAmB,EACnB,YAAY,EACZ,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,YAAY,CAAA;AACnB,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AACjF,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC/D,YAAY,EAAE,cAAc,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAG3F,mBAAmB,YAAY,CAAA"}
|