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.
Files changed (44) hide show
  1. package/LICENSE +201 -0
  2. package/README.es.md +197 -0
  3. package/README.ja.md +197 -0
  4. package/README.ko.md +197 -0
  5. package/README.md +197 -0
  6. package/README.zh.md +197 -0
  7. package/cordis.patch.yml +40 -0
  8. package/docs/VERIFICATION.zh.md +93 -0
  9. package/lib/client.js +83 -0
  10. package/lib/index.js +415 -0
  11. package/lib/invariant-LV6hQX5s.js +442 -0
  12. package/lib/invariant.js +2 -0
  13. package/lib/types/client/index.d.ts +30 -0
  14. package/lib/types/client/index.d.ts.map +1 -0
  15. package/lib/types/client/locales.d.ts +13 -0
  16. package/lib/types/client/locales.d.ts.map +1 -0
  17. package/lib/types/config.d.ts +73 -0
  18. package/lib/types/config.d.ts.map +1 -0
  19. package/lib/types/index.d.ts +31 -0
  20. package/lib/types/index.d.ts.map +1 -0
  21. package/lib/types/invariant.d.ts +59 -0
  22. package/lib/types/invariant.d.ts.map +1 -0
  23. package/lib/types/runtime.d.ts +144 -0
  24. package/lib/types/runtime.d.ts.map +1 -0
  25. package/lib/types/style-command.d.ts +68 -0
  26. package/lib/types/style-command.d.ts.map +1 -0
  27. package/lib/types/style-library.d.ts +78 -0
  28. package/lib/types/style-library.d.ts.map +1 -0
  29. package/lib/types/types.d.ts +78 -0
  30. package/lib/types/types.d.ts.map +1 -0
  31. package/package.json +138 -0
  32. package/src/client/index.ts +104 -0
  33. package/src/client/locales.ts +14 -0
  34. package/src/config.ts +110 -0
  35. package/src/index.ts +51 -0
  36. package/src/invariant.ts +144 -0
  37. package/src/runtime.ts +439 -0
  38. package/src/style-command.ts +89 -0
  39. package/src/style-library.ts +348 -0
  40. package/src/types.ts +86 -0
  41. package/styles/concise.md +17 -0
  42. package/styles/explanatory.md +14 -0
  43. package/styles/formal.md +14 -0
  44. package/styles/step-by-step.md +16 -0
@@ -0,0 +1,104 @@
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
+
12
+ import type { ClientContext, ISessions } from '@deepseek-ai/dsh-client-runtime/client'
13
+ import type {} from '@deepseek-ai/dsh-api-remotes/client'
14
+ import type {} from '@deepseek-ai/dsh-client-locale/client'
15
+ import type { CommandUiContract, SelectOption } from '@deepseek-ai/dsh-client-ui-commands/client'
16
+ import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
17
+ import type { StyleSelectionView } from '../types.ts'
18
+ import { en, zh, type StyleKey } from './locales.ts'
19
+
20
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
21
+ interface LocaleNamespaceMap {
22
+ /** The style picker's copy. */
23
+ style: StyleKey
24
+ }
25
+ }
26
+
27
+ /** Client plugin name; keep stable after publishing. */
28
+ export const name = 'dsh-output-styles-client'
29
+
30
+ /** Required client services: the command surface, the sessions face, the command Remote, and locale. */
31
+ export const inject = ['commandUi', 'locale', 'remote', 'sessions']
32
+
33
+ /** Dictionary namespace owned by this plugin. */
34
+ const NS = 'style'
35
+
36
+ /** The picker row that restores the project default; stable, never a style name. */
37
+ const OFF_ID = 'off'
38
+
39
+ /** Flatten the projection into picker rows: the off row first, then one row per style. */
40
+ function optionsOf(view: StyleSelectionView, t: TranslateNS<typeof NS>): SelectOption[] {
41
+ const rows: SelectOption[] = [{
42
+ id: OFF_ID,
43
+ label: t('option.off'),
44
+ detail: t('option.offDetail'),
45
+ active: view.currentValue === null,
46
+ }]
47
+ for (const option of view.options) {
48
+ rows.push({
49
+ id: option.value,
50
+ label: option.name,
51
+ detail: option.whenToUse !== undefined
52
+ ? `${option.description} · ${option.whenToUse}`
53
+ : option.description,
54
+ active: view.currentValue === option.value,
55
+ })
56
+ }
57
+ return rows
58
+ }
59
+
60
+ /**
61
+ * Client plugin body: register the `style` dictionaries and decorate the
62
+ * host `/style` command's bare invocation with the projection-backed picker.
63
+ * @param ctx - client root context.
64
+ */
65
+ export function apply(ctx: ClientContext): void {
66
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-output-styles: style dictionaries')
67
+ const t = ctx.locale.bind(NS)
68
+
69
+ ctx.inject(['commandUi', 'remote', 'sessions'], (scope: ClientContext) => {
70
+ const commandUi = scope.get('commandUi') as CommandUiContract
71
+ // The host graph (`dsh-session`) and the client graph (`dsh-client-runtime`)
72
+ // both declare a `sessions` service on Context; this package compiles
73
+ // against both faces, so the runtime value is the client service named
74
+ // 'sessions' and the host type is what the merged declaration resolves.
75
+ const sessions = scope.get('sessions') as unknown as ISessions
76
+ const remote = scope.remote
77
+
78
+ scope.effect(() => commandUi.decorate({
79
+ name: 'style',
80
+ available: () => true,
81
+ ui: {
82
+ kind: 'popupSelect',
83
+ options: async (session) => {
84
+ const binding = sessions.binding(session.sessionId)
85
+ const view = binding?.session.projections.faceOf('style').getSnapshot() as StyleSelectionView | undefined
86
+ if (view === undefined) return []
87
+ return optionsOf(view, t)
88
+ },
89
+ onSelect: async (option, session) => {
90
+ const line = option.id === OFF_ID ? '/style off' : `/style ${option.id}`
91
+ const result = await remote.commands.execute(session.sessionId, line)
92
+ if (!result.ok) {
93
+ throw new Error(`command.execute failed: ${result.error.code}: ${result.error.message}`)
94
+ }
95
+ if (result.value === undefined) {
96
+ throw new Error(`unknown or malformed command: ${line}`)
97
+ }
98
+ },
99
+ },
100
+ }), 'dsh-output-styles: /style picker')
101
+ })
102
+ }
103
+
104
+ export type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
@@ -0,0 +1,14 @@
1
+ /** Simplified Chinese dictionary (the key-set source of truth). */
2
+ export const zh = {
3
+ 'option.off': '关闭(默认)',
4
+ 'option.offDetail': '恢复项目默认输出风格',
5
+ } satisfies Record<string, string>
6
+
7
+ /** The style picker namespace key union. */
8
+ export type StyleKey = keyof typeof zh
9
+
10
+ /** English dictionary, checked complete against the zh key set. */
11
+ export const en = {
12
+ 'option.off': 'Off (default)',
13
+ 'option.offDetail': 'Restore the project default output style',
14
+ } satisfies Record<StyleKey, string>
package/src/config.ts ADDED
@@ -0,0 +1,110 @@
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
+
11
+ import z from '@deepseek-ai/schemastery'
12
+ import { resolve } from 'node:path'
13
+
14
+ /** Plugin configuration supplied by the profile composition. */
15
+ export interface Config {
16
+ /**
17
+ * Directories holding the style library (`*.md`, and with {@link Config.compatJson}
18
+ * also `*.json`). Each entry resolves against the process working directory.
19
+ * Later directories override earlier ones on a same-named style; the bundled
20
+ * `styles/` directory participates as the lowest-priority entry unless
21
+ * {@link Config.includeBuiltins} is false. An empty list means the bundled
22
+ * library only (or none, with `includeBuiltins: false`). A bare string is
23
+ * accepted as a single-directory list.
24
+ */
25
+ stylesDir?: string | string[]
26
+ /** Style-body budget in characters; longer bodies are truncated at the budget with a marker. */
27
+ maxStyleChars?: number
28
+ /**
29
+ * Style injected into sessions that never selected one (and no project
30
+ * settings default exists). The empty string (default) means new sessions
31
+ * get no style — the session's own selection, made through `/style`, is
32
+ * always what wins for a session that has one.
33
+ */
34
+ defaultStyle?: string
35
+ /** Load Claude Code `outputStyles` JSON entries (`{ name, description, prompt }`) beside Markdown styles. */
36
+ compatJson?: boolean
37
+ /** Order of the injected system-prompt section (90: after the persona, before tool guidance at 100–199). */
38
+ sectionOrder?: number
39
+ /** Marker appended at the truncation point when a style body exceeds {@link Config.maxStyleChars}. */
40
+ truncationMarker?: string
41
+ /** Include the package's bundled `styles/` directory in the library. */
42
+ includeBuiltins?: boolean
43
+ /** Reload the library when a style file changes on disk (default true). */
44
+ watchStyles?: boolean
45
+ }
46
+
47
+ /** Configuration after defaults have been resolved. */
48
+ export interface ResolvedConfig {
49
+ /** Absolute style-library directories, lowest priority first (bundled styles first when included). */
50
+ stylesDirs: string[]
51
+ /** Style-body budget in characters; at least 1. */
52
+ maxStyleChars: number
53
+ /** Style injected into sessions that never selected one; `''` means none. */
54
+ defaultStyle: string
55
+ /** Whether Claude Code `outputStyles` JSON entries are loaded. */
56
+ compatJson: boolean
57
+ /** Order of the injected system-prompt section; a finite number. */
58
+ sectionOrder: number
59
+ /** Marker appended at the truncation point. */
60
+ truncationMarker: string
61
+ /** Whether the bundled `styles/` directory participates. */
62
+ includeBuiltins: boolean
63
+ /** Whether the library reloads on style-file changes. */
64
+ watchStyles: boolean
65
+ }
66
+
67
+ /** Loader-visible configuration schema and defaults. */
68
+ export const Config: z<Config> = z.object({
69
+ stylesDir: z.union([z.string(), z.array(z.string())]).default([]),
70
+ maxStyleChars: z.number().min(1).default(4000),
71
+ defaultStyle: z.string().default(''),
72
+ compatJson: z.boolean().default(true),
73
+ sectionOrder: z.number().default(90),
74
+ truncationMarker: z.string().default('\n\n[style truncated]'),
75
+ includeBuiltins: z.boolean().default(true),
76
+ watchStyles: z.boolean().default(true),
77
+ })
78
+
79
+ /**
80
+ * Resolve the same defaults for direct callers that bypass the Cordis Loader,
81
+ * and fail loud on values the schema cannot express (a non-finite section
82
+ * order).
83
+ * @param config - Partial serialized configuration.
84
+ * @param defaultStylesDir - Absolute bundled directory used when built-ins are included.
85
+ * @returns Configuration with every default applied.
86
+ */
87
+ export function resolveConfig(config: Config, defaultStylesDir: string): ResolvedConfig {
88
+ const maxStyleChars = config.maxStyleChars ?? 4000
89
+ if (!Number.isFinite(maxStyleChars) || maxStyleChars < 1) {
90
+ throw new Error(`dsh-output-styles: maxStyleChars must be a finite number ≥ 1, got ${String(config.maxStyleChars)}`)
91
+ }
92
+ const sectionOrder = config.sectionOrder ?? 90
93
+ if (!Number.isFinite(sectionOrder)) {
94
+ throw new Error(`dsh-output-styles: sectionOrder must be a finite number, got ${String(config.sectionOrder)}`)
95
+ }
96
+ const includeBuiltins = config.includeBuiltins ?? true
97
+ const rawDirs = Array.isArray(config.stylesDir) ? config.stylesDir : config.stylesDir === undefined || config.stylesDir === '' ? [] : [config.stylesDir]
98
+ const customDirs = rawDirs.map(dir => resolve(dir))
99
+ const stylesDirs = includeBuiltins ? [defaultStylesDir, ...customDirs] : customDirs
100
+ return {
101
+ stylesDirs,
102
+ maxStyleChars,
103
+ defaultStyle: config.defaultStyle ?? '',
104
+ compatJson: config.compatJson ?? true,
105
+ sectionOrder,
106
+ truncationMarker: config.truncationMarker ?? '\n\n[style truncated]',
107
+ includeBuiltins,
108
+ watchStyles: config.watchStyles ?? true,
109
+ }
110
+ }
package/src/index.ts ADDED
@@ -0,0 +1,51 @@
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
+
10
+ /** Cordis plugin name; keep this stable after publishing. */
11
+ export const name = 'dsh-output-styles'
12
+
13
+ /**
14
+ * Services that must exist before the plugin applies: the prompt-assembly
15
+ * registry for the injected section, and the storage domain facility for
16
+ * per-session persistence. A composition without a routed kv backend keeps
17
+ * the plugin pending until the storage rows appear (Cordis dependency
18
+ * semantics), instead of racing a parallel mount.
19
+ */
20
+ export const inject = ['systemPrompt', 'storageDomain']
21
+
22
+ export { Config, resolveConfig } from './config.ts'
23
+ export type { ResolvedConfig } from './config.ts'
24
+ export { apply, DEFAULT_STYLES_DIR, OutputStyleRuntime, STYLE_SECTION_NAME } from './runtime.ts'
25
+ export {
26
+ applyStyleEvent,
27
+ EMPTY_STYLE_STATE,
28
+ parseStyleInput,
29
+ STYLE_COMMAND,
30
+ } from './style-command.ts'
31
+ export type { StyleFoldState, StyleInput } from './style-command.ts'
32
+ export {
33
+ isValidStyleName,
34
+ loadStyleLibrary,
35
+ STYLE_NAME_RE,
36
+ truncateStyle,
37
+ } from './style-library.ts'
38
+ export type { OutputStyle } from './style-library.ts'
39
+ export {
40
+ OFF,
41
+ OUTPUT_STYLE_DOMAIN,
42
+ STYLE_SOURCE,
43
+ styleSelectionSchema,
44
+ styleSelectionViewSchema,
45
+ } from './types.ts'
46
+ export type { StyleOption, StyleSelection, StyleSelectionView } from './types.ts'
47
+ export { installInvariant, PACKAGE_NAME } from './invariant.ts'
48
+ export type { InvariantFacts, InvariantInstaller, InvariantRegistry } from './invariant.ts'
49
+ // Type-only re-export: keeps the `style` SessionProjectionMap merge edge in
50
+ // the emitted index.d.ts, so consumers receive the projection key's type.
51
+ export type * from './types.ts'
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Package-owned invariant companion for `dsh-output-styles`.
3
+ *
4
+ * Two post-commit diagnostic checks over the events this plugin's write path
5
+ * produces (both target events are committed before dispatch, so a violation
6
+ * is reported — via the host registry's `fail` — rather than vetoed):
7
+ *
8
+ * 1. Every `output_style`/`selection` durable write carries this plugin's own
9
+ * source marker and a legal style name; when the library is known,
10
+ * the name must be a library member.
11
+ * 2. A successful `/style <name>` command has a matching selection record on
12
+ * its session by the time its `command/done` settles, and `/style off`
13
+ * leaves no record. The standalone companion (no library/domain handle)
14
+ * skips this check.
15
+ *
16
+ * The main plugin registers a facts-bearing installer from its own context;
17
+ * the `./invariant` export is the standalone companion usable through a
18
+ * separate profile row.
19
+ * @module dsh-output-styles/invariant
20
+ */
21
+
22
+ import type { Context } from '@deepseek-ai/cordis'
23
+ import type { Session, SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
24
+ import type {} from '@deepseek-ai/dsh-commands'
25
+ import type { DomainChanged } from '@deepseek-ai/dsh-storage-domain'
26
+ import { parseStyleInput, STYLE_COMMAND } from './style-command.ts'
27
+ import { isValidStyleName } from './style-library.ts'
28
+ import { OFF, STYLE_SOURCE } from './types.ts'
29
+
30
+ /** Full npm package name owning the reported failures. */
31
+ export const PACKAGE_NAME = 'dsh-output-styles'
32
+
33
+ /** A package-attributed invariant failure reported by the host registry. */
34
+ export type InvariantFailure = (message: string) => never
35
+
36
+ /** Facts the installer needs beyond the event stream. */
37
+ export interface InvariantFacts {
38
+ /** Library style names, or undefined when the companion has no library handle. */
39
+ knownStyles(): ReadonlySet<string> | undefined
40
+ /** One session's durable selection record; absent disables the pairing check. */
41
+ selectionFor?(sessionId: SessionId): { style: string } | undefined
42
+ }
43
+
44
+ /** Installer callback accepted by the host's invariant registry. */
45
+ export type InvariantInstaller = (ctx: Context, fail: InvariantFailure) => void | Promise<void>
46
+
47
+ /** Minimal runtime contract used by the companion without a source checkout. */
48
+ export interface InvariantRegistry {
49
+ register(packageName: string, installer: InvariantInstaller): () => void
50
+ }
51
+
52
+ /** Cordis companion plugin name. */
53
+ export const name = 'dsh-output-styles-invariant'
54
+ /** Service required before the companion can reserve package ownership. */
55
+ export const inject = ['invariants']
56
+
57
+ /** Facts for the standalone companion: envelope checks only, no library/domain handle. */
58
+ const COMPANION_FACTS: InvariantFacts = {
59
+ knownStyles: () => undefined,
60
+ }
61
+
62
+ /** One `/style` command run awaiting its `command/done`. */
63
+ interface PendingSwitch {
64
+ session: Session
65
+ expected: { name: string } | { off: true }
66
+ }
67
+
68
+ /**
69
+ * Build the installer over a facts source. The standalone companion and the
70
+ * main plugin share this body; only the facts differ.
71
+ * @param facts - library and domain access for the checks.
72
+ * @returns the installer the host registry activates in its child context.
73
+ */
74
+ export function installInvariant(facts: InvariantFacts): InvariantInstaller {
75
+ return (ctx, fail) => {
76
+ const pending = new Map<string, PendingSwitch>()
77
+
78
+ ctx.on('domain/changed', (change: DomainChanged) => {
79
+ if (change.domain !== 'output_style' || change.table !== 'selection' || change.operation !== 'put') return
80
+ const value = change.value as { style?: unknown; source?: unknown }
81
+ if (typeof value.style !== 'string' || !isValidStyleName(value.style)) {
82
+ fail(`selection record names invalid style ${JSON.stringify(value.style)}`)
83
+ }
84
+ if (JSON.stringify(value.source) !== JSON.stringify(STYLE_SOURCE)) {
85
+ fail(`selection record source is ${JSON.stringify(value.source)}; expected this plugin's own marker`)
86
+ }
87
+ const known = facts.knownStyles()
88
+ if (known !== undefined && typeof value.style === 'string' && !known.has(value.style)) {
89
+ fail(`selection record names style "${value.style}" that is not in the style library`)
90
+ }
91
+ })
92
+
93
+ ctx.on('session/event', (session: Session, event: SessionEvent) => {
94
+ if (event.type === 'command/run'
95
+ && event.data.name === STYLE_COMMAND
96
+ && event.data.args !== undefined) {
97
+ const input = parseStyleInput(event.data.args)
98
+ if (input.kind === 'off') {
99
+ pending.set(String(event.data.commandId), { session, expected: { off: true } })
100
+ } else if (input.kind === 'switch') {
101
+ pending.set(String(event.data.commandId), { session, expected: { name: input.name } })
102
+ }
103
+ return
104
+ }
105
+ if (event.type !== 'command/done') return
106
+ const entry = pending.get(String(event.data.commandId))
107
+ if (entry === undefined) return
108
+ pending.delete(String(event.data.commandId))
109
+ if (event.data.kind !== 'success' || facts.selectionFor === undefined) return
110
+ const record = facts.selectionFor(entry.session.id)
111
+ if ('off' in entry.expected) {
112
+ if (record !== undefined) {
113
+ fail(`/style ${OFF} settled on session "${entry.session.id}" but its selection record still exists`)
114
+ }
115
+ } else if (record === undefined || record.style !== entry.expected.name) {
116
+ fail(`/style ${entry.expected.name} settled on session "${entry.session.id}" without a matching selection record`)
117
+ }
118
+ }, { global: true })
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Resolve the host registry through Cordis's named service lookup. Keeping
124
+ * this narrow local contract lets the companion build without host source
125
+ * files; a composed DSH profile still supplies the real `invariants` service.
126
+ * @param ctx - Cordis context carrying the host service.
127
+ * @returns the host invariant registry.
128
+ * @throws {Error} when the companion is loaded without its host service.
129
+ */
130
+ function getInvariantRegistry(ctx: Context): InvariantRegistry {
131
+ const registry = ctx.get('invariants') as InvariantRegistry | undefined
132
+ if (registry === undefined) {
133
+ throw new Error(`invariant companion requires the "invariants" service for ${PACKAGE_NAME}`)
134
+ }
135
+ return registry
136
+ }
137
+
138
+ /**
139
+ * Register the standalone companion with envelope-only facts.
140
+ * @param ctx - Cordis context carrying the invariant service.
141
+ * @returns the installed registration's disposer after setup succeeds.
142
+ */
143
+ export const apply = (ctx: Context): Promise<() => void> =>
144
+ Promise.resolve(getInvariantRegistry(ctx).register(PACKAGE_NAME, installInvariant(COMPANION_FACTS)))