dsh-output-styles 0.4.3 → 0.5.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/CHANGELOG.md CHANGED
@@ -4,6 +4,30 @@ All notable changes to this project are documented in this file. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.5.0] - 2026-08-23
8
+
9
+ ### Added
10
+
11
+ - **`/export --save <path>`**: the export command now writes the rendered
12
+ document to a workspace path, gated by the user-approval service and the fs
13
+ service. `/export [md|markdown|html] [--renderer=<id>] [--save <path>]` — the
14
+ no-argument behavior is unchanged (the document is returned as output text);
15
+ `--save` writes only after `ctx.get('approval')` grants `allowed-once`
16
+ (fail-closed when the service is absent, rejects, cancels, or throws), uses
17
+ `ctx.get('fs')` for the actual write (fail-loud with a structured error when
18
+ absent), and passes the document through `sanitizeText` before writing.
19
+ `md` is accepted as a Markdown alias. `@deepseek-ai/dsh-fs` and
20
+ `@deepseek-ai/dsh-user-approval` are declared as optional peer dependencies.
21
+
22
+ ### Changed
23
+
24
+ - **Standards alignment**: `package.json` declares `packageManager` (`pnpm@11.7.0`,
25
+ matching CI and the lockfile), and the `cordis.patch.yml` reference comment
26
+ now lists the two renderer-protocol config keys (`rules`, `enableExport`)
27
+ added in 0.4.0. No runtime behavior changes.
28
+ - Five READMEs: `/export --save` reference, the `fs:write` workshop permission,
29
+ and the test count refreshed to 127.
30
+
7
31
  ## [0.4.3] - 2026-08-22
8
32
 
9
33
  ### Changed
package/README.es.md CHANGED
@@ -40,7 +40,7 @@
40
40
  - **Paridad Claude Code** — `keep-coding-instructions`, `force-for-plugin` (alias `force`), compatibilidad JSON `outputStyles`, directorios `stylesDir` por capas, recarga en caliente y fallback del proyecto sobre la costura de settings de DSH.
41
41
  - **Registro de renderers (`output.render.*`)** — `ctx.outputRenderers` permite a cualquier plugin registrar un presenter puro, aplicado a través de la cascada `output.render/before`; renderers integrados `concise` y `step-by-step`.
42
42
  - **Reglas por sesión/por herramienta** — `rules: [{ match: { tool: 'bash' }, style: 'concise' }]` nombran el renderer para solicitudes coincidentes; editables mediante la sección de settings `output-style-rules`.
43
- - **`/export`** — renderiza la sesión actual a Markdown o HTML saneado a través de la tubería de render; cada render conserva el texto original junto al renderizado.
43
+ - **`/export`** — renderiza la sesión actual a Markdown o HTML saneado a través de la tubería de render; `--save <path>` escribe el documento saneado en esa ruta de workspace tras la aprobación del usuario. Cada render conserva el texto original junto al renderizado.
44
44
 
45
45
  ## Quick start
46
46
 
@@ -111,14 +111,14 @@ Todos los parámetros son campos Schemastery `Config` (modificables desde cordis
111
111
  | `includeBuiltins` | `true` | Incluir los `styles/` del paquete como capa de menor prioridad |
112
112
  | `watchStyles` | `true` | Recargar la librería cuando un archivo de estilo cambia en disco |
113
113
  | `rules` | `[]` | Reglas de render por sesión/herramienta: `[{ match: { tool?, contentType?, session? }, style, priority? }]` |
114
- | `enableExport` | `true` | Registrar el comando `/export` (exportación de sesión Markdown/HTML, consciente del renderer) |
114
+ | `enableExport` | `true` | Registrar el comando `/export` (exportación de sesión Markdown/HTML, consciente del renderer; `--save` escribe con aprobación) |
115
115
 
116
116
  ## Tools & surfaces
117
117
 
118
118
  | Surface | Kind | Notes |
119
119
  |---|---|---|
120
120
  | `/style` | command | Lista estilos, cambia o restaura el valor por defecto del proyecto |
121
- | `/export` | command | Renderiza la sesión actual a Markdown o HTML saneado |
121
+ | `/export` | command | Renderiza la sesión actual a Markdown o HTML saneado; `--save` escribe con aprobación |
122
122
  | `output_style` | storage domain | Elección de estilo por sesión, indexada por sessionId |
123
123
  | `systemPrompt.section()` | contribution | Inyecta el cuerpo del estilo actual en cada ensamblado |
124
124
  | `output.render.*` | renderer registry | `ctx.outputRenderers` + la cascada `output.render/before` |
@@ -135,8 +135,10 @@ Todos los parámetros son campos Schemastery `Config` (modificables desde cordis
135
135
  | `/style off` | Restaura el valor por defecto del proyecto (default de settings, luego `defaultStyle`) |
136
136
  | `/style nope` | `error: unknown output style "nope" (available: …)` |
137
137
  | `/export` | Renderiza la sesión actual a Markdown a través de la pipeline de render |
138
+ | `/export md` | Renderiza a Markdown (`md` es la forma abreviada de `markdown`) |
138
139
  | `/export html` | Renderiza a HTML saneado |
139
140
  | `/export --renderer=concise` | Renderiza forzando un renderer (reglas omitidas) |
141
+ | `/export md --save report.md` | Renderiza y luego escribe el documento saneado en `report.md` tras la aprobación |
140
142
 
141
143
  ## Style library
142
144
 
@@ -182,7 +184,7 @@ Filtrado contra el ecosistema DSH antes del desarrollo (instantánea 2026-08): n
182
184
 
183
185
  ## Permissions & data
184
186
 
185
- - **Permissions**: el manifiesto de workshop declara `fs:read`, `fs:watch`, `storage:read`, `storage:write` y `settings:read`.
187
+ - **Permissions**: el manifiesto de workshop declara `fs:read`, `fs:write`, `fs:watch`, `storage:read`, `storage:write` y `settings:read`.
186
188
  - **Data**: la elección de estilo vive en el dominio de almacenamiento `output_style` (indexada por sessionId); no se persiste otro estado, sin solicitudes de red.
187
189
  - **Session log**: el nombre del estilo viene de `command/run`, el texto exacto inyectado de `request/header`; el marcador de procedencia `{ kind: 'plugin', plugin: 'dsh-output-styles' }` viaja en el registro del dominio.
188
190
 
@@ -191,6 +193,7 @@ Filtrado contra el ecosistema DSH antes del desarrollo (instantánea 2026-08): n
191
193
  - **Solo servicios públicos.** Contribuye `systemPrompt`, comandos, almacenamiento y settings; sin cambios en engine / agent-loop / apiproxy / UI oficial.
192
194
  - **Visible para el modelo ⟺ registrado.** Todo lo que el modelo ve es reconstruible desde el registro de sesión — sin un nuevo tipo de evento de sesión, sin cambios en el agent-loop.
193
195
  - **Original siempre conservado.** Cada render (y `/export`) conserva el texto original junto al renderizado; para la exportación HTML se usa HTML saneado.
196
+ - **Escrituras en disco controladas.** `/export --save` escribe solo después de que el servicio de aprobación lo conceda, y el contenido escrito pasa primero por la función pura `sanitizeText`; sin un servicio de aprobación o fs no escribe nada (fail-closed).
194
197
 
195
198
  ## Known limitations
196
199
 
@@ -203,7 +206,7 @@ Filtrado contra el ecosistema DSH antes del desarrollo (instantánea 2026-08): n
203
206
  ```sh
204
207
  pnpm install
205
208
  pnpm run typecheck # ambos proyectos tsc
206
- pnpm test # vitest — 107 tests
209
+ pnpm test # vitest — 127 tests
207
210
  pnpm run verify # typecheck + tests + self-contained (la puerta de prepublishOnly)
208
211
  pnpm run build # artefactos lib/ (bundles host + client)
209
212
  pnpm pack # tarball para dsh plugin add
package/README.hi.md CHANGED
@@ -40,7 +40,7 @@
40
40
  - **Claude Code समानता** — `keep-coding-instructions`, `force-for-plugin` (उपनाम `force`), `outputStyles` JSON संगतता, स्तरित `stylesDir` निर्देशिकाएँ, हॉट रीलोड और DSH settings सीम पर परियोजना-डिफ़ॉल्ट फ़ॉलबैक।
41
41
  - **रेंडरर रजिस्ट्री (`output.render.*`)** — `ctx.outputRenderers` किसी भी प्लगइन को एक शुद्ध presenter पंजीकृत करने देता है, जो `output.render/before` वॉटरफ़ॉल से लागू होता है; अंतर्निहित रेंडरर `concise` और `step-by-step`।
42
42
  - **प्रति-सत्र/प्रति-टूल नियम** — `rules: [{ match: { tool: 'bash' }, style: 'concise' }]` मिलान वाले अनुरोधों के लिए रेंडरर नामित करते हैं; `output-style-rules` settings अनुभाग से संपादन-योग्य।
43
- - **`/export`** — रेंडर पाइपलाइन से वर्तमान सत्र को Markdown या सैनिटाइज़्ड HTML में प्रस्तुत करता है; हर रेंडर मूल पाठ को रेंडर किए गए के साथ रखता है।
43
+ - **`/export`** — रेंडर पाइपलाइन से वर्तमान सत्र को Markdown या सैनिटाइज़्ड HTML में प्रस्तुत करता है; `--save <path>` उपयोगकर्ता की स्वीकृति के बाद सैनिटाइज़्ड दस्तावेज़ को उस workspace पथ पर लिखता है। हर रेंडर मूल पाठ को रेंडर किए गए के साथ रखता है।
44
44
 
45
45
  ## Quick start
46
46
 
@@ -111,14 +111,14 @@ flowchart LR
111
111
  | `includeBuiltins` | `true` | पैकेज के अंतर्निहित `styles/` को निम्नतम-प्राथमिकता परत के रूप में शामिल करें |
112
112
  | `watchStyles` | `true` | डिस्क पर शैली फ़ाइल बदलने पर पुस्तकालय फिर से लोड करें |
113
113
  | `rules` | `[]` | प्रति-सत्र/प्रति-टूल रेंडर नियम: `[{ match: { tool?, contentType?, session? }, style, priority? }]` |
114
- | `enableExport` | `true` | `/export` कमांड पंजीकृत करें (Markdown/HTML सत्र निर्यात, रेंडरर-जागरूक) |
114
+ | `enableExport` | `true` | `/export` कमांड पंजीकृत करें (Markdown/HTML सत्र निर्यात, रेंडरर-जागरूक; `--save` स्वीकृति से लिखता है) |
115
115
 
116
116
  ## Tools & surfaces
117
117
 
118
118
  | Surface | Kind | Notes |
119
119
  |---|---|---|
120
120
  | `/style` | command | शैलियाँ सूचीबद्ध करें, बदलें या परियोजना डिफ़ॉल्ट बहाल करें |
121
- | `/export` | command | वर्तमान सत्र को Markdown या सैनिटाइज़्ड HTML में प्रस्तुत करें |
121
+ | `/export` | command | वर्तमान सत्र को Markdown या सैनिटाइज़्ड HTML में प्रस्तुत करें; `--save` स्वीकृति से लिखता है |
122
122
  | `output_style` | storage domain | sessionId से अनुक्रमित सत्र-स्कोप्ड शैली चयन |
123
123
  | `systemPrompt.section()` | contribution | हर संयोजन पर वर्तमान शैली का मुख्य भाग इंजेक्ट करता है |
124
124
  | `output.render.*` | renderer registry | `ctx.outputRenderers` + `output.render/before` वॉटरफ़ॉल |
@@ -135,8 +135,10 @@ flowchart LR
135
135
  | `/style off` | परियोजना डिफ़ॉल्ट बहाल करें (settings डिफ़ॉल्ट, फिर `defaultStyle`) |
136
136
  | `/style nope` | `error: unknown output style "nope" (available: …)` |
137
137
  | `/export` | रेंडर पाइपलाइन से वर्तमान सत्र को Markdown में प्रस्तुत करें |
138
+ | `/export md` | Markdown में प्रस्तुत करें (`md`, `markdown` का संक्षिप्त रूप है) |
138
139
  | `/export html` | सैनिटाइज़्ड HTML में प्रस्तुत करें |
139
140
  | `/export --renderer=concise` | एक रेंडरर बाध्य करके प्रस्तुत करें (नियम छोड़े गए) |
141
+ | `/export md --save report.md` | प्रस्तुत करें, फिर स्वीकृति के बाद सैनिटाइज़्ड दस्तावेज़ को `report.md` में लिखें |
140
142
 
141
143
  ## Style library
142
144
 
@@ -182,7 +184,7 @@ flowchart LR
182
184
 
183
185
  ## Permissions & data
184
186
 
185
- - **Permissions**: workshop मैनिफ़ेस्ट `fs:read`, `fs:watch`, `storage:read`, `storage:write` और `settings:read` घोषित करता है।
187
+ - **Permissions**: workshop मैनिफ़ेस्ट `fs:read`, `fs:write`, `fs:watch`, `storage:read`, `storage:write` और `settings:read` घोषित करता है।
186
188
  - **Data**: शैली चयन `output_style` स्टोरेज डोमेन में रहता है (sessionId से अनुक्रमित); कोई अन्य स्थिति स्थायी नहीं, कोई नेटवर्क अनुरोध नहीं।
187
189
  - **Session log**: शैली नाम `command/run` से आता है, सटीक इंजेक्ट किया गया पाठ `request/header` से; स्रोत मार्कर `{ kind: 'plugin', plugin: 'dsh-output-styles' }` डोमेन रिकॉर्ड में चलता है।
188
190
 
@@ -191,6 +193,7 @@ flowchart LR
191
193
  - **केवल सार्वजनिक सेवाएँ।** `systemPrompt`, कमांड, स्टोरेज और settings योगदान करता है; engine / agent-loop / apiproxy / आधिकारिक UI में कोई बदलाव नहीं।
192
194
  - **मॉडल-दृश्य ⟺ लॉग किया गया।** मॉडल जो देखता है वह सब सत्र लॉग से पुनर्निर्माण-योग्य है — कोई नया सत्र घटना प्रकार नहीं, कोई agent-loop बदलाव नहीं।
193
195
  - **मूल हमेशा रखा गया।** हर रेंडर (और `/export`) मूल पाठ को रेंडर किए गए के साथ रखता है; HTML निर्यात के लिए सैनिटाइज़्ड HTML उपयोग होता है।
196
+ - **डिस्क लेखन गेटेड।** `/export --save` केवल स्वीकृति सेवा की अनुमति के बाद लिखता है, और लिखा गया कंटेंट पहले `sanitizeText` शुद्ध फ़ंक्शन से गुज़रता है; स्वीकृति या fs सेवा के बिना कुछ भी नहीं लिखा जाता (fail-closed)।
194
197
 
195
198
  ## Known limitations
196
199
 
@@ -203,7 +206,7 @@ flowchart LR
203
206
  ```sh
204
207
  pnpm install
205
208
  pnpm run typecheck # दोनों tsc परियोजनाएँ
206
- pnpm test # vitest — 107 tests
209
+ pnpm test # vitest — 127 tests
207
210
  pnpm run verify # typecheck + tests + self-contained (prepublishOnly द्वार)
208
211
  pnpm run build # lib/ कलाकृतियाँ (host + client बंडल)
209
212
  pnpm pack # dsh plugin add के लिए tarball
package/README.md CHANGED
@@ -41,7 +41,7 @@
41
41
  - **Claude Code parity** — `keep-coding-instructions`, `force-for-plugin` (`force` alias), `outputStyles` JSON compatibility, layered `stylesDir` directories, hot reload, and project-default fallback over the DSH settings seam.
42
42
  - **Renderer registry (`output.render.*`)** — `ctx.outputRenderers` lets any plugin register a pure presenter, applied through the `output.render/before` waterfall; built-in renderers `concise` and `step-by-step`.
43
43
  - **Per-session/per-tool rules** — `rules: [{ match: { tool: 'bash' }, style: 'concise' }]` name the renderer for matching requests; editable through the `output-style-rules` settings section.
44
- - **`/export`** — render the current session to Markdown or sanitized HTML through the render pipeline; every render keeps the original text beside the rendered one.
44
+ - **`/export`** — render the current session to Markdown or sanitized HTML through the render pipeline; `--save <path>` writes the sanitized document to that workspace path after user approval. Every render keeps the original text beside the rendered one.
45
45
 
46
46
  ## Quick start
47
47
 
@@ -112,14 +112,14 @@ All tunables are Schemastery `Config` fields (changeable from cordis.yml). Inval
112
112
  | `includeBuiltins` | `true` | Include the package's bundled `styles/` as the lowest-priority layer |
113
113
  | `watchStyles` | `true` | Reload the library when a style file changes on disk |
114
114
  | `rules` | `[]` | Per-session/per-tool render rules: `[{ match: { tool?, contentType?, session? }, style, priority? }]` |
115
- | `enableExport` | `true` | Register the `/export` command (Markdown/HTML session export, renderer-aware) |
115
+ | `enableExport` | `true` | Register the `/export` command (Markdown/HTML session export, renderer-aware; `--save` writes with approval) |
116
116
 
117
117
  ## Tools & surfaces
118
118
 
119
119
  | Surface | Kind | Notes |
120
120
  |---|---|---|
121
121
  | `/style` | command | List styles, switch, or restore the project default |
122
- | `/export` | command | Render the current session to Markdown or sanitized HTML |
122
+ | `/export` | command | Render the current session to Markdown or sanitized HTML; `--save` writes with approval |
123
123
  | `output_style` | storage domain | Session-scoped style choice, keyed by sessionId |
124
124
  | `systemPrompt.section()` | contribution | Injects the current style body at every assembly |
125
125
  | `output.render.*` | renderer registry | `ctx.outputRenderers` + the `output.render/before` waterfall |
@@ -136,8 +136,10 @@ All tunables are Schemastery `Config` fields (changeable from cordis.yml). Inval
136
136
  | `/style off` | Restore the project default (settings default, then `defaultStyle`) |
137
137
  | `/style nope` | `error: unknown output style "nope" (available: …)` |
138
138
  | `/export` | Render the current session to Markdown through the renderer pipeline |
139
+ | `/export md` | Render to Markdown (`md` is the shorthand for `markdown`) |
139
140
  | `/export html` | Render to sanitized HTML |
140
141
  | `/export --renderer=concise` | Render with one renderer forced (rules bypassed) |
142
+ | `/export md --save report.md` | Render, then write the sanitized document to `report.md` after approval |
141
143
 
142
144
  ## Style library
143
145
 
@@ -183,7 +185,7 @@ Screened against the DSH ecosystem before development (2026-08 snapshot): no `st
183
185
 
184
186
  ## Permissions & data
185
187
 
186
- - **Permissions**: declares `fs:read`, `fs:watch`, `storage:read`, `storage:write`, and `settings:read` in its workshop manifest.
188
+ - **Permissions**: declares `fs:read`, `fs:write`, `fs:watch`, `storage:read`, `storage:write`, and `settings:read` in its workshop manifest.
187
189
  - **Data**: the style choice lives in the `output_style` storage domain (keyed by sessionId); no other state is persisted, no network requests.
188
190
  - **Session log**: the style name comes from `command/run`, the exact injected text from `request/header`; the provenance marker `{ kind: 'plugin', plugin: 'dsh-output-styles' }` rides in the domain record.
189
191
 
@@ -192,6 +194,7 @@ Screened against the DSH ecosystem before development (2026-08 snapshot): no `st
192
194
  - **Public services only.** Contributes `systemPrompt`, commands, storage, and settings; no engine / agent-loop / apiproxy / official-UI changes.
193
195
  - **Model-visible ⟺ logged.** Everything the model sees is reconstructable from the session log — no new session event type, no agent-loop changes.
194
196
  - **Original always kept.** Every render (and `/export`) keeps the original text beside the rendered one; sanitized HTML is used for HTML export.
197
+ - **Disk writes gated.** `/export --save` writes only after the approval service grants it, and the written content passes through the `sanitizeText` pure function first; without an approval or fs service it writes nothing (fail-closed).
195
198
 
196
199
  ## Known limitations
197
200
 
@@ -204,7 +207,7 @@ Screened against the DSH ecosystem before development (2026-08 snapshot): no `st
204
207
  ```sh
205
208
  pnpm install
206
209
  pnpm run typecheck # both tsc projects
207
- pnpm test # vitest — 107 tests
210
+ pnpm test # vitest — 127 tests
208
211
  pnpm run verify # typecheck + tests + self-contained (the prepublishOnly gate)
209
212
  pnpm run build # lib/ artifacts (host + client bundles)
210
213
  pnpm pack # tarball for dsh plugin add
package/README.pt.md CHANGED
@@ -40,7 +40,7 @@ O `dsh-output-styles` é o equivalente do `outputStyles` do Claude Code para o D
40
40
  - **Paridade Claude Code** — `keep-coding-instructions`, `force-for-plugin` (alias `force`), compatibilidade JSON `outputStyles`, diretórios `stylesDir` em camadas, recarga a quente e fallback do projeto sobre a costura de settings do DSH.
41
41
  - **Registro de renderers (`output.render.*`)** — `ctx.outputRenderers` permite a qualquer plugin registrar um presenter puro, aplicado pela cascata `output.render/before`; renderers integrados `concise` e `step-by-step`.
42
42
  - **Regras por sessão/por ferramenta** — `rules: [{ match: { tool: 'bash' }, style: 'concise' }]` nomeiam o renderer para solicitações coincidentes; editáveis pela seção de settings `output-style-rules`.
43
- - **`/export`** — renderiza a sessão atual para Markdown ou HTML saneado pela pipeline de render; cada render mantém o texto original ao lado do renderizado.
43
+ - **`/export`** — renderiza a sessão atual para Markdown ou HTML saneado pela pipeline de render; `--save <path>` escreve o documento saneado nessa rota de workspace após aprovação do usuário. Cada render mantém o texto original ao lado do renderizado.
44
44
 
45
45
  ## Quick start
46
46
 
@@ -111,14 +111,14 @@ Todos os parâmetros são campos Schemastery `Config` (alteráveis pelo cordis.y
111
111
  | `includeBuiltins` | `true` | Incluir os `styles/` do pacote como camada de menor prioridade |
112
112
  | `watchStyles` | `true` | Recarregar a biblioteca quando um arquivo de estilo muda em disco |
113
113
  | `rules` | `[]` | Regras de render por sessão/ferramenta: `[{ match: { tool?, contentType?, session? }, style, priority? }]` |
114
- | `enableExport` | `true` | Registrar o comando `/export` (exportação de sessão Markdown/HTML, ciente do renderer) |
114
+ | `enableExport` | `true` | Registrar o comando `/export` (exportação de sessão Markdown/HTML, ciente do renderer; `--save` escreve com aprovação) |
115
115
 
116
116
  ## Tools & surfaces
117
117
 
118
118
  | Surface | Kind | Notes |
119
119
  |---|---|---|
120
120
  | `/style` | command | Lista estilos, alterna ou restaura o padrão do projeto |
121
- | `/export` | command | Renderiza a sessão atual para Markdown ou HTML saneado |
121
+ | `/export` | command | Renderiza a sessão atual para Markdown ou HTML saneado; `--save` escreve com aprovação |
122
122
  | `output_style` | storage domain | Escolha de estilo por sessão, indexada por sessionId |
123
123
  | `systemPrompt.section()` | contribution | Injeta o corpo do estilo atual a cada montagem |
124
124
  | `output.render.*` | renderer registry | `ctx.outputRenderers` + a cascata `output.render/before` |
@@ -135,8 +135,10 @@ Todos os parâmetros são campos Schemastery `Config` (alteráveis pelo cordis.y
135
135
  | `/style off` | Restaura o padrão do projeto (default de settings, depois `defaultStyle`) |
136
136
  | `/style nope` | `error: unknown output style "nope" (available: …)` |
137
137
  | `/export` | Renderiza a sessão atual para Markdown pela pipeline de render |
138
+ | `/export md` | Renderiza para Markdown (`md` é a forma abreviada de `markdown`) |
138
139
  | `/export html` | Renderiza para HTML saneado |
139
140
  | `/export --renderer=concise` | Renderiza forçando um renderer (regras ignoradas) |
141
+ | `/export md --save report.md` | Renderiza e então escreve o documento saneado em `report.md` após aprovação |
140
142
 
141
143
  ## Style library
142
144
 
@@ -182,7 +184,7 @@ Filtrado contra o ecossistema DSH antes do desenvolvimento (instantânea 2026-08
182
184
 
183
185
  ## Permissions & data
184
186
 
185
- - **Permissions**: o manifesto de workshop declara `fs:read`, `fs:watch`, `storage:read`, `storage:write` e `settings:read`.
187
+ - **Permissions**: o manifesto de workshop declara `fs:read`, `fs:write`, `fs:watch`, `storage:read`, `storage:write` e `settings:read`.
186
188
  - **Data**: a escolha de estilo vive no domínio de armazenamento `output_style` (indexada por sessionId); nenhum outro estado é persistido, sem solicitações de rede.
187
189
  - **Session log**: o nome do estilo vem de `command/run`, o texto exato injetado de `request/header`; o marcador de procedência `{ kind: 'plugin', plugin: 'dsh-output-styles' }` viaja no registro do domínio.
188
190
 
@@ -191,6 +193,7 @@ Filtrado contra o ecossistema DSH antes do desenvolvimento (instantânea 2026-08
191
193
  - **Somente serviços públicos.** Contribui `systemPrompt`, comandos, armazenamento e settings; sem alterações em engine / agent-loop / apiproxy / UI oficial.
192
194
  - **Visível para o modelo ⟺ registrado.** Tudo o que o modelo vê é reconstruível a partir do log de sessão — sem novo tipo de evento de sessão, sem alterações no agent-loop.
193
195
  - **Original sempre conservado.** Cada render (e `/export`) mantém o texto original ao lado do renderizado; a exportação HTML usa HTML saneado.
196
+ - **Escritas em disco controladas.** `/export --save` escreve somente após o serviço de aprovação conceder, e o conteúdo escrito passa primeiro pela função pura `sanitizeText`; sem um serviço de aprovação ou fs, nada é escrito (fail-closed).
194
197
 
195
198
  ## Known limitations
196
199
 
@@ -203,7 +206,7 @@ Filtrado contra o ecossistema DSH antes do desenvolvimento (instantânea 2026-08
203
206
  ```sh
204
207
  pnpm install
205
208
  pnpm run typecheck # ambos os projetos tsc
206
- pnpm test # vitest — 107 tests
209
+ pnpm test # vitest — 127 tests
207
210
  pnpm run verify # typecheck + tests + self-contained (a porta de prepublishOnly)
208
211
  pnpm run build # artefatos lib/ (bundles host + client)
209
212
  pnpm pack # tarball para dsh plugin add
package/README.zh.md CHANGED
@@ -40,7 +40,7 @@
40
40
  - **Claude Code 对齐** —— `keep-coding-instructions`、`force-for-plugin`(别名 `force`)、`outputStyles` JSON 兼容、分层 `stylesDir` 目录、热重载,以及通过 DSH settings 接缝的项目默认回退。
41
41
  - **渲染器注册表(`output.render.*`)** —— `ctx.outputRenderers` 允许任意插件注册纯 presenter,经 `output.render/before` waterfall 应用;内置渲染器 `concise` 与 `step-by-step`。
42
42
  - **按会话/按工具规则** —— `rules: [{ match: { tool: 'bash' }, style: 'concise' }]` 为匹配请求指定渲染器;可通过 `output-style-rules` 设置区编辑。
43
- - **`/export`** —— 经渲染管线把当前会话导出为 Markdown 或净化 HTML;每次渲染都保留原文与渲染结果并列。
43
+ - **`/export`** —— 经渲染管线把当前会话导出为 Markdown 或净化 HTML;`--save <path>` 经用户审批后把净化文档写入该工作区路径。每次渲染都保留原文与渲染结果并列。
44
44
 
45
45
  ## Quick start
46
46
 
@@ -111,14 +111,14 @@ flowchart LR
111
111
  | `includeBuiltins` | `true` | 将包内置 `styles/` 作为最低优先级层 |
112
112
  | `watchStyles` | `true` | 风格文件在磁盘上变化时重载库 |
113
113
  | `rules` | `[]` | 按会话/按工具渲染规则:`[{ match: { tool?, contentType?, session? }, style, priority? }]` |
114
- | `enableExport` | `true` | 注册 `/export` 命令(Markdown/HTML 会话导出,感知渲染器) |
114
+ | `enableExport` | `true` | 注册 `/export` 命令(Markdown/HTML 会话导出,感知渲染器;`--save` 经审批写入) |
115
115
 
116
116
  ## Tools & surfaces
117
117
 
118
118
  | Surface | Kind | Notes |
119
119
  |---|---|---|
120
120
  | `/style` | command | 列出风格、切换或恢复项目默认 |
121
- | `/export` | command | 把当前会话渲染为 Markdown 或净化 HTML |
121
+ | `/export` | command | 把当前会话渲染为 Markdown 或净化 HTML;`--save` 经审批写入 |
122
122
  | `output_style` | storage domain | 按 sessionId 隔离的会话级风格选择 |
123
123
  | `systemPrompt.section()` | contribution | 在每次组装时注入当前风格正文 |
124
124
  | `output.render.*` | renderer registry | `ctx.outputRenderers` + `output.render/before` waterfall |
@@ -135,8 +135,10 @@ flowchart LR
135
135
  | `/style off` | 恢复项目默认(settings 默认,其次 `defaultStyle`) |
136
136
  | `/style nope` | `error: unknown output style "nope" (available: …)` |
137
137
  | `/export` | 经渲染管线把当前会话渲染为 Markdown |
138
+ | `/export md` | 渲染为 Markdown(`md` 是 `markdown` 的简写) |
138
139
  | `/export html` | 渲染为净化 HTML |
139
140
  | `/export --renderer=concise` | 强制指定一个渲染器渲染(跳过规则) |
141
+ | `/export md --save report.md` | 渲染后经审批把净化文档写入 `report.md` |
140
142
 
141
143
  ## Style library
142
144
 
@@ -182,7 +184,7 @@ flowchart LR
182
184
 
183
185
  ## Permissions & data
184
186
 
185
- - **Permissions**:workshop 清单声明 `fs:read`、`fs:watch`、`storage:read`、`storage:write` 与 `settings:read`。
187
+ - **Permissions**:workshop 清单声明 `fs:read`、`fs:write`、`fs:watch`、`storage:read`、`storage:write` 与 `settings:read`。
186
188
  - **Data**:风格选择存于 `output_style` 存储域(按 sessionId 隔离);不持久化其他状态,无网络请求。
187
189
  - **Session log**:风格名来自 `command/run`,精确注入文本来自 `request/header`;来源标记 `{ kind: 'plugin', plugin: 'dsh-output-styles' }` 随域记录携带。
188
190
 
@@ -191,6 +193,7 @@ flowchart LR
191
193
  - **仅公开服务。** 贡献 `systemPrompt`、命令、存储与 settings;不改 engine / agent-loop / apiproxy / 官方 UI。
192
194
  - **模型可见 ⟺ 已记录。** 模型所见的一切都能从会话日志重建 —— 无新增会话事件类型、无 agent-loop 改动。
193
195
  - **始终保留原文。** 每次渲染(含 `/export`)都保留原文与渲染结果并列;HTML 导出使用净化 HTML。
196
+ - **写盘有门禁。** `/export --save` 仅在审批服务放行后写入,且写入内容先经 `sanitizeText` 纯函数净化;缺少审批或 fs 服务时一律不写入(fail-closed)。
194
197
 
195
198
  ## Known limitations
196
199
 
@@ -203,7 +206,7 @@ flowchart LR
203
206
  ```sh
204
207
  pnpm install
205
208
  pnpm run typecheck # 两个 tsc 项目
206
- pnpm test # vitest —— 107 个测试
209
+ pnpm test # vitest —— 127 个测试
207
210
  pnpm run verify # typecheck + tests + self-contained(prepublishOnly 门禁)
208
211
  pnpm run build # lib/ 产物(host + client 包)
209
212
  pnpm pack # 供 dsh plugin add 的 tarball
package/cordis.patch.yml CHANGED
@@ -31,6 +31,11 @@
31
31
  # truncationMarker: "\n\n[style truncated]"
32
32
  # includeBuiltins: true
33
33
  # watchStyles: true
34
+ # rules: [] # per-session/per-tool render rules
35
+ # # [{ match: { tool?, contentType?, session? }, style, priority? }]
36
+ # enableExport: true # register the /export command
37
+ # # (md|html; --save <path> writes the
38
+ # # sanitized document after approval)
34
39
 
35
40
  # Optional invariant companion (envelope-level checks). Only for profiles
36
41
  # that DISABLE the main row above: the main plugin already registers
@@ -132,3 +132,13 @@ export function apply(ctx: Context): void {
132
132
  const result = await ctx.outputRenderers.renderText(rawText, { tool: 'sql', contentType: 'text' })
133
133
  // { original, rendered, rendererId, changed } — log both halves wherever you surface it.
134
134
  ```
135
+
136
+ ## Export to disk
137
+
138
+ `/export` returns the rendered document as command output text. `/export
139
+ [md|markdown|html] [--renderer=<id>] --save <path>` additionally writes the
140
+ document to a workspace path: the document passes through the `sanitizeText`
141
+ pure function first, then the write is gated by the approval service
142
+ (`ctx.get('approval')`, fail-closed when absent) and performed by the fs
143
+ service (`ctx.get('fs')`, fail-loud when absent). The render pipeline itself is
144
+ unchanged — the same presenters and rule table apply before either output.
@@ -123,3 +123,11 @@ export function apply(ctx: Context): void {
123
123
  const result = await ctx.outputRenderers.renderText(rawText, { tool: 'sql', contentType: 'text' })
124
124
  // { original, rendered, rendererId, changed } —— 在任何展示它的地方把两半都记下来。
125
125
  ```
126
+
127
+ ## 导出到磁盘
128
+
129
+ `/export` 把渲染后的文档作为命令输出文本返回。`/export [md|markdown|html]
130
+ [--renderer=<id>] --save <path>` 另外把文档写入工作区路径:文档先经 `sanitizeText`
131
+ 纯函数净化,随后写入由审批服务(`ctx.get('approval')`,缺失则 fail-closed)把关、由
132
+ fs 服务(`ctx.get('fs')`,缺失则大声失败)执行。渲染流水线本身不变——两种输出之前都应用
133
+ 同样的 presenter 与规则表。
package/lib/index.js CHANGED
@@ -794,12 +794,12 @@ async function apply(ctx, config) {
794
794
  commandCtx.commands.register({
795
795
  name: "export",
796
796
  description: "Export this session as Markdown or HTML (renderer-aware)",
797
- input: { hint: "[markdown|html] [--renderer=<id>]" },
798
- handler: async ({ agent, rawInput }) => {
797
+ input: { hint: "[markdown|html] [--renderer=<id>] [--save <path>]" },
798
+ handler: async ({ agent, rawInput, signal }) => {
799
799
  const input = parseExportInput(rawInput);
800
800
  if (input.kind === "error") return {
801
801
  kind: "error",
802
- text: "usage: /export [markdown|html] [--renderer=<id>]"
802
+ text: "usage: /export [markdown|html] [--renderer=<id>] [--save <path>]"
803
803
  };
804
804
  const lines = conversationLines(agent.session.events);
805
805
  const rules = input.renderer === void 0 ? [...effectiveRules] : [{
@@ -807,28 +807,57 @@ async function apply(ctx, config) {
807
807
  style: input.renderer,
808
808
  priority: 0
809
809
  }];
810
+ const document = renderExport(renderers, lines, input.format, rules);
811
+ if (input.save === void 0) return {
812
+ kind: "success",
813
+ text: document.text
814
+ };
815
+ const saved = await saveExportFile(ctx.get("fs"), ctx.get("approval"), agent, input.save, sanitizeText(document.text), signal);
816
+ if (saved.kind === "error") return {
817
+ kind: "error",
818
+ text: saved.text
819
+ };
810
820
  return {
811
821
  kind: "success",
812
- text: renderExport(renderers, lines, input.format, rules).text
822
+ text: `saved ${input.format} export to ${saved.path}`
813
823
  };
814
824
  }
815
825
  });
816
826
  });
817
827
  }
818
- /** Parse `/export [markdown|html] [--renderer=<id>]` from the raw command input. */
828
+ /** Parse `/export [markdown|html] [--renderer=<id>] [--save <path>]` from the raw command input. */
819
829
  function parseExportInput(rawInput) {
820
830
  const raw = String(rawInput ?? "").trim();
821
831
  const parts = raw === "" ? [] : raw.split(/\s+/);
822
832
  let format = "markdown";
823
833
  let renderer;
824
- for (const part of parts) {
825
- if (part === "markdown" || part === "html") {
826
- format = part;
834
+ let save;
835
+ for (let index = 0; index < parts.length; index += 1) {
836
+ const part = parts[index];
837
+ if (part === void 0) continue;
838
+ if (part === "markdown" || part === "md") {
839
+ format = "markdown";
840
+ continue;
841
+ }
842
+ if (part === "html") {
843
+ format = "html";
844
+ continue;
845
+ }
846
+ const rendererMatch = /^--renderer=([a-z0-9][a-z0-9-]*)$/.exec(part);
847
+ if (rendererMatch !== null) {
848
+ renderer = rendererMatch[1];
827
849
  continue;
828
850
  }
829
- const match = /^--renderer=([a-z0-9][a-z0-9-]*)$/.exec(part);
830
- if (match !== null) {
831
- renderer = match[1];
851
+ const saveInline = /^--save=(.+)$/.exec(part);
852
+ if (saveInline !== null) {
853
+ save = saveInline[1];
854
+ continue;
855
+ }
856
+ if (part === "--save") {
857
+ const next = parts[index + 1];
858
+ if (next === void 0 || next === "") return { kind: "error" };
859
+ save = next;
860
+ index += 1;
832
861
  continue;
833
862
  }
834
863
  return { kind: "error" };
@@ -836,7 +865,69 @@ function parseExportInput(rawInput) {
836
865
  return {
837
866
  kind: "ok",
838
867
  format,
839
- ...renderer === void 0 ? {} : { renderer }
868
+ ...renderer === void 0 ? {} : { renderer },
869
+ ...save === void 0 ? {} : { save }
870
+ };
871
+ }
872
+ /**
873
+ * Write one rendered export document to a workspace path after user approval.
874
+ * Fail-closed: a missing approval service, a rejected/cancelled/unavailable
875
+ * decision, or a throwing approval channel all deny the write; a missing fs
876
+ * service fails loudly with a structured error. The caller already sanitized
877
+ * `content` before this write.
878
+ * @param fs - the fs service (`ctx.get('fs')`), or undefined when none is composed.
879
+ * @param approval - the approval service (`ctx.get('approval')`), or undefined when none is composed.
880
+ * @param agent - the agent whose session the export belongs to (routes the approval).
881
+ * @param path - the workspace path to write.
882
+ * @param content - the sanitized document text.
883
+ * @param signal - the command's abort signal, forwarded to approval and resolve.
884
+ * @returns the written path, or a structured failure.
885
+ */
886
+ async function saveExportFile(fs, approval, agent, path, content, signal) {
887
+ if (approval === void 0) return {
888
+ kind: "error",
889
+ code: "approval-unavailable",
890
+ text: "dsh-output-styles: /export --save requires an approval service (compose @deepseek-ai/dsh-user-approval); nothing was written"
891
+ };
892
+ let outcome;
893
+ try {
894
+ outcome = await approval.request({
895
+ agent,
896
+ toolName: "export",
897
+ reason: `write the exported document to ${path}`,
898
+ ...signal === void 0 ? {} : { signal }
899
+ });
900
+ } catch {
901
+ outcome = "unavailable";
902
+ }
903
+ switch (outcome) {
904
+ case "allowed-once": break;
905
+ case "rejected": return {
906
+ kind: "error",
907
+ code: "approval-denied",
908
+ text: "dsh-output-styles: /export --save was rejected; nothing was written"
909
+ };
910
+ case "cancelled": return {
911
+ kind: "error",
912
+ code: "approval-cancelled",
913
+ text: "dsh-output-styles: /export --save was cancelled; nothing was written"
914
+ };
915
+ default: return {
916
+ kind: "error",
917
+ code: "approval-unavailable",
918
+ text: "dsh-output-styles: /export --save approval is unavailable; nothing was written"
919
+ };
920
+ }
921
+ if (fs === void 0) return {
922
+ kind: "error",
923
+ code: "fs-unavailable",
924
+ text: "dsh-output-styles: /export --save requires an fs service (compose @deepseek-ai/dsh-fs); nothing was written"
925
+ };
926
+ const target = await fs.resolve(path, signal === void 0 ? void 0 : { signal });
927
+ await fs.writeText(target, content);
928
+ return {
929
+ kind: "written",
930
+ path
840
931
  };
841
932
  }
842
933
  //#endregion
@@ -146,10 +146,56 @@ type ExportInput = {
146
146
  kind: 'ok';
147
147
  format: 'markdown' | 'html';
148
148
  renderer?: string;
149
+ save?: string;
149
150
  } | {
150
151
  kind: 'error';
151
152
  };
152
- /** Parse `/export [markdown|html] [--renderer=<id>]` from the raw command input. */
153
+ /** Parse `/export [markdown|html] [--renderer=<id>] [--save <path>]` from the raw command input. */
153
154
  export declare function parseExportInput(rawInput: unknown): ExportInput;
155
+ /** Approval outcome vocabulary, structural (mirrors the approval seam without importing it). */
156
+ type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable';
157
+ /** Structural slice of the DSH filesystem service the save path uses. */
158
+ export interface ExportFileSystem {
159
+ resolve(path: string, opts?: {
160
+ cwd?: string;
161
+ signal?: AbortSignal;
162
+ }): Promise<unknown>;
163
+ writeText(target: unknown, content: string): Promise<unknown>;
164
+ }
165
+ /** Structural slice of the DSH approval service the save path uses. */
166
+ export interface ExportApproval {
167
+ request(request: {
168
+ agent: unknown;
169
+ toolName: string;
170
+ reason: string;
171
+ signal?: AbortSignal;
172
+ }): Promise<ApprovalOutcome>;
173
+ }
174
+ /** Error codes of the `/export --save` path; each is a stable machine-readable label. */
175
+ export type ExportSaveErrorCode = 'fs-unavailable' | 'approval-unavailable' | 'approval-denied' | 'approval-cancelled';
176
+ /** Result of a `/export --save` attempt: the written path or a structured failure. */
177
+ export type ExportSaveResult = {
178
+ readonly kind: 'written';
179
+ readonly path: string;
180
+ } | {
181
+ readonly kind: 'error';
182
+ readonly code: ExportSaveErrorCode;
183
+ readonly text: string;
184
+ };
185
+ /**
186
+ * Write one rendered export document to a workspace path after user approval.
187
+ * Fail-closed: a missing approval service, a rejected/cancelled/unavailable
188
+ * decision, or a throwing approval channel all deny the write; a missing fs
189
+ * service fails loudly with a structured error. The caller already sanitized
190
+ * `content` before this write.
191
+ * @param fs - the fs service (`ctx.get('fs')`), or undefined when none is composed.
192
+ * @param approval - the approval service (`ctx.get('approval')`), or undefined when none is composed.
193
+ * @param agent - the agent whose session the export belongs to (routes the approval).
194
+ * @param path - the workspace path to write.
195
+ * @param content - the sanitized document text.
196
+ * @param signal - the command's abort signal, forwarded to approval and resolve.
197
+ * @returns the written path, or a structured failure.
198
+ */
199
+ export declare function saveExportFile(fs: ExportFileSystem | undefined, approval: ExportApproval | undefined, agent: unknown, path: string, content: string, signal?: AbortSignal): Promise<ExportSaveResult>;
154
200
  export {};
155
201
  //# sourceMappingURL=runtime.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/runtime.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/runtime.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-output-styles",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "description": "Claude Code outputStyles-equivalent runtime output-style switching for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -17,6 +17,7 @@
17
17
  "publishConfig": {
18
18
  "access": "public"
19
19
  },
20
+ "packageManager": "pnpm@11.7.0",
20
21
  "engines": {
21
22
  "node": "^22.19.0 || >=24.0.0"
22
23
  },
@@ -99,6 +100,7 @@
99
100
  },
100
101
  "permissions": [
101
102
  "fs:read",
103
+ "fs:write",
102
104
  "fs:watch",
103
105
  "storage:read",
104
106
  "storage:write",
@@ -131,13 +133,21 @@
131
133
  },
132
134
  "peerDependencies": {
133
135
  "@deepseek-ai/cordis": "^4.0.1",
136
+ "@deepseek-ai/dsh-fs": ">=0.1.0-rc.8 <0.2.0",
134
137
  "@deepseek-ai/dsh-session": ">=0.1.0-rc.8 <0.2.0",
135
138
  "@deepseek-ai/dsh-session-projection": ">=0.1.1-rc.2 <0.2.0",
136
139
  "@deepseek-ai/dsh-settings": ">=0.1.0-rc.8 <0.2.0",
137
140
  "@deepseek-ai/dsh-storage-domain": ">=0.1.0-rc.8 <0.2.0",
141
+ "@deepseek-ai/dsh-user-approval": ">=0.1.0-rc.8 <0.2.0",
138
142
  "@deepseek-ai/schemastery": "^3.18.1"
139
143
  },
140
144
  "peerDependenciesMeta": {
145
+ "@deepseek-ai/dsh-fs": {
146
+ "optional": true
147
+ },
148
+ "@deepseek-ai/dsh-user-approval": {
149
+ "optional": true
150
+ },
141
151
  "@deepseek-ai/dsh-settings": {
142
152
  "optional": true
143
153
  },
package/src/runtime.ts CHANGED
@@ -25,7 +25,7 @@ import { loadStyleLibrary, truncateStyle, type OutputStyle } from './style-libra
25
25
  import { applyStyleEvent, EMPTY_STYLE_STATE, parseStyleInput, STYLE_COMMAND, type StyleFoldState } from './style-command.ts'
26
26
  import { OUTPUT_STYLE_DOMAIN, STYLE_SOURCE, styleFoldStateSchema, styleSelectionViewSchema, type StyleSelection, type StyleSelectionView } from './types.ts'
27
27
  import { BUILTIN_RENDERERS, RendererRegistry, type OutputRenderer, type RenderContext, type RenderedText, type StyleRule } from './renderers.ts'
28
- import { conversationLines, renderExport } from './export.ts'
28
+ import { conversationLines, renderExport, sanitizeText } from './export.ts'
29
29
 
30
30
  /** Bundled style-library directory (package `styles/`), the lowest-priority `stylesDir` entry. */
31
31
  export const DEFAULT_STYLES_DIR = fileURLToPath(new URL('../styles/', import.meta.url))
@@ -507,23 +507,41 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
507
507
  // Markdown or sanitized HTML through the renderer pipeline. The document
508
508
  // itself is the visible artifact; the original lines are the session log
509
509
  // the export was projected from — rendered and original stay reconstructable.
510
+ // `--save <path>` additionally writes the sanitized document to that
511
+ // workspace path, gated by the approval service and the fs service (both
512
+ // optional; a missing approval service denies the write, a missing fs
513
+ // service fails loudly).
510
514
  if (resolved.enableExport) {
511
515
  ctx.inject(['commands'], (commandCtx) => {
512
516
  commandCtx.commands.register({
513
517
  name: 'export',
514
518
  description: 'Export this session as Markdown or HTML (renderer-aware)',
515
- input: { hint: '[markdown|html] [--renderer=<id>]' },
516
- handler: async ({ agent, rawInput }) => {
519
+ input: { hint: '[markdown|html] [--renderer=<id>] [--save <path>]' },
520
+ handler: async ({ agent, rawInput, signal }) => {
517
521
  const input = parseExportInput(rawInput)
518
522
  if (input.kind === 'error') {
519
- return { kind: 'error', text: 'usage: /export [markdown|html] [--renderer=<id>]' }
523
+ return { kind: 'error', text: 'usage: /export [markdown|html] [--renderer=<id>] [--save <path>]' }
520
524
  }
521
525
  const lines = conversationLines(agent.session.events)
522
526
  const rules: StyleRule[] = input.renderer === undefined
523
527
  ? [...effectiveRules]
524
528
  : [{ match: {}, style: input.renderer, priority: 0 }]
525
529
  const document = renderExport(renderers, lines, input.format, rules)
526
- return { kind: 'success', text: document.text }
530
+ if (input.save === undefined) {
531
+ return { kind: 'success', text: document.text }
532
+ }
533
+ // The disk path sanitizes the rendered document before writing and
534
+ // writes only after the approval service grants it.
535
+ const saved = await saveExportFile(
536
+ ctx.get('fs') as ExportFileSystem | undefined,
537
+ ctx.get('approval') as ExportApproval | undefined,
538
+ agent,
539
+ input.save,
540
+ sanitizeText(document.text),
541
+ signal,
542
+ )
543
+ if (saved.kind === 'error') return { kind: 'error', text: saved.text }
544
+ return { kind: 'success', text: `saved ${input.format} export to ${saved.path}` }
527
545
  },
528
546
  })
529
547
  })
@@ -531,25 +549,151 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
531
549
  }
532
550
 
533
551
  /** Parsed `/export` invocation. */
534
- type ExportInput = { kind: 'ok'; format: 'markdown' | 'html'; renderer?: string } | { kind: 'error' }
535
-
536
- /** Parse `/export [markdown|html] [--renderer=<id>]` from the raw command input. */
552
+ type ExportInput = {
553
+ kind: 'ok'
554
+ format: 'markdown' | 'html'
555
+ renderer?: string
556
+ save?: string
557
+ } | { kind: 'error' }
558
+
559
+ /** Parse `/export [markdown|html] [--renderer=<id>] [--save <path>]` from the raw command input. */
537
560
  export function parseExportInput(rawInput: unknown): ExportInput {
538
561
  const raw = String(rawInput ?? '').trim()
539
562
  const parts = raw === '' ? [] : raw.split(/\s+/)
540
563
  let format: 'markdown' | 'html' = 'markdown'
541
564
  let renderer: string | undefined
542
- for (const part of parts) {
543
- if (part === 'markdown' || part === 'html') {
544
- format = part
565
+ let save: string | undefined
566
+ for (let index = 0; index < parts.length; index += 1) {
567
+ const part = parts[index]
568
+ if (part === undefined) continue
569
+ if (part === 'markdown' || part === 'md') {
570
+ format = 'markdown'
571
+ continue
572
+ }
573
+ if (part === 'html') {
574
+ format = 'html'
575
+ continue
576
+ }
577
+ const rendererMatch = /^--renderer=([a-z0-9][a-z0-9-]*)$/.exec(part)
578
+ if (rendererMatch !== null) {
579
+ renderer = rendererMatch[1]
580
+ continue
581
+ }
582
+ const saveInline = /^--save=(.+)$/.exec(part)
583
+ if (saveInline !== null) {
584
+ save = saveInline[1]
545
585
  continue
546
586
  }
547
- const match = /^--renderer=([a-z0-9][a-z0-9-]*)$/.exec(part)
548
- if (match !== null) {
549
- renderer = match[1]
587
+ if (part === '--save') {
588
+ const next = parts[index + 1]
589
+ if (next === undefined || next === '') return { kind: 'error' }
590
+ save = next
591
+ index += 1
550
592
  continue
551
593
  }
552
594
  return { kind: 'error' }
553
595
  }
554
- return { kind: 'ok', format, ...renderer === undefined ? {} : { renderer } }
596
+ return {
597
+ kind: 'ok',
598
+ format,
599
+ ...renderer === undefined ? {} : { renderer },
600
+ ...save === undefined ? {} : { save },
601
+ }
602
+ }
603
+
604
+ /** Approval outcome vocabulary, structural (mirrors the approval seam without importing it). */
605
+ type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable'
606
+
607
+ /** Structural slice of the DSH filesystem service the save path uses. */
608
+ export interface ExportFileSystem {
609
+ resolve(path: string, opts?: { cwd?: string; signal?: AbortSignal }): Promise<unknown>
610
+ writeText(target: unknown, content: string): Promise<unknown>
611
+ }
612
+
613
+ /** Structural slice of the DSH approval service the save path uses. */
614
+ export interface ExportApproval {
615
+ request(request: { agent: unknown; toolName: string; reason: string; signal?: AbortSignal }): Promise<ApprovalOutcome>
616
+ }
617
+
618
+ /** Error codes of the `/export --save` path; each is a stable machine-readable label. */
619
+ export type ExportSaveErrorCode = 'fs-unavailable' | 'approval-unavailable' | 'approval-denied' | 'approval-cancelled'
620
+
621
+ /** Result of a `/export --save` attempt: the written path or a structured failure. */
622
+ export type ExportSaveResult =
623
+ | { readonly kind: 'written'; readonly path: string }
624
+ | { readonly kind: 'error'; readonly code: ExportSaveErrorCode; readonly text: string }
625
+
626
+ /**
627
+ * Write one rendered export document to a workspace path after user approval.
628
+ * Fail-closed: a missing approval service, a rejected/cancelled/unavailable
629
+ * decision, or a throwing approval channel all deny the write; a missing fs
630
+ * service fails loudly with a structured error. The caller already sanitized
631
+ * `content` before this write.
632
+ * @param fs - the fs service (`ctx.get('fs')`), or undefined when none is composed.
633
+ * @param approval - the approval service (`ctx.get('approval')`), or undefined when none is composed.
634
+ * @param agent - the agent whose session the export belongs to (routes the approval).
635
+ * @param path - the workspace path to write.
636
+ * @param content - the sanitized document text.
637
+ * @param signal - the command's abort signal, forwarded to approval and resolve.
638
+ * @returns the written path, or a structured failure.
639
+ */
640
+ export async function saveExportFile(
641
+ fs: ExportFileSystem | undefined,
642
+ approval: ExportApproval | undefined,
643
+ agent: unknown,
644
+ path: string,
645
+ content: string,
646
+ signal?: AbortSignal,
647
+ ): Promise<ExportSaveResult> {
648
+ if (approval === undefined) {
649
+ return {
650
+ kind: 'error',
651
+ code: 'approval-unavailable',
652
+ text: 'dsh-output-styles: /export --save requires an approval service (compose @deepseek-ai/dsh-user-approval); nothing was written',
653
+ }
654
+ }
655
+ let outcome: ApprovalOutcome
656
+ try {
657
+ outcome = await approval.request({
658
+ agent,
659
+ toolName: 'export',
660
+ reason: `write the exported document to ${path}`,
661
+ ...signal === undefined ? {} : { signal },
662
+ })
663
+ } catch {
664
+ // An approval channel that cannot answer is an unanswerable ask: fail closed.
665
+ outcome = 'unavailable'
666
+ }
667
+ switch (outcome) {
668
+ case 'allowed-once': break
669
+ case 'rejected':
670
+ return {
671
+ kind: 'error',
672
+ code: 'approval-denied',
673
+ text: 'dsh-output-styles: /export --save was rejected; nothing was written',
674
+ }
675
+ case 'cancelled':
676
+ return {
677
+ kind: 'error',
678
+ code: 'approval-cancelled',
679
+ text: 'dsh-output-styles: /export --save was cancelled; nothing was written',
680
+ }
681
+ default:
682
+ // 'unavailable' plus any out-of-vocabulary answer fail closed (never write).
683
+ return {
684
+ kind: 'error',
685
+ code: 'approval-unavailable',
686
+ text: 'dsh-output-styles: /export --save approval is unavailable; nothing was written',
687
+ }
688
+ }
689
+ if (fs === undefined) {
690
+ return {
691
+ kind: 'error',
692
+ code: 'fs-unavailable',
693
+ text: 'dsh-output-styles: /export --save requires an fs service (compose @deepseek-ai/dsh-fs); nothing was written',
694
+ }
695
+ }
696
+ const target = await fs.resolve(path, signal === undefined ? undefined : { signal })
697
+ await fs.writeText(target, content)
698
+ return { kind: 'written', path }
555
699
  }