dsh-persona-forge 0.0.0-stage → 0.2.1
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 +216 -0
- package/LICENSE +21 -0
- package/README.md +404 -2
- package/README.zh-CN.md +297 -0
- package/cards/muscle-crew.yml +100 -0
- package/cards/omnissiah.yml +98 -0
- package/cordis.patch.yml +11 -0
- package/lib/client.js +1598 -0
- package/lib/http.js +31 -0
- package/lib/index.js +169 -0
- package/lib/loopback.js +56 -0
- package/lib/prompts.js +218 -0
- package/lib/rewrite.js +188 -0
- package/lib/routes.js +446 -0
- package/lib/shared/protocol.js +45 -0
- package/lib/store.js +596 -0
- package/lib/yaml.js +527 -0
- package/package.json +73 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
5
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
Nothing yet.
|
|
10
|
+
|
|
11
|
+
## [0.2.1] - 2026-10-09
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **Check for changes**, in the character-card manager. It compares every user
|
|
16
|
+
card against the bundled card of the same id and reports only the ones that
|
|
17
|
+
actually differ; only a drifted card grows a **Restore bundled** action on its
|
|
18
|
+
row. The alternative — a restore button on every user card — turns a long list
|
|
19
|
+
into noise, and "restore" on a card that never changed is a no-op nobody
|
|
20
|
+
asked for.
|
|
21
|
+
- The drift report names the differing fields (`style`, `examples`, …).
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **The card row's description overlapped the buttons.** `.pf-card-desc` is a
|
|
26
|
+
`<span>`, and `overflow: hidden` / `text-overflow: ellipsis` do not apply to
|
|
27
|
+
inline elements, so a long description — such as the bundled 硬邦邦 card's —
|
|
28
|
+
spilled out of the card instead of being clipped. The body is now a flex
|
|
29
|
+
column, which blockifies its children, and the description is clamped to two
|
|
30
|
+
lines. The row's actions are `flex: none`, so the description yields rather
|
|
31
|
+
than the buttons.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- The drift comparison is by **content, not bytes**. Bundled cards are
|
|
36
|
+
hand-written YAML carrying comments, while a saved card round-trips through
|
|
37
|
+
the store's serializer and loses them, so a byte comparison reported every
|
|
38
|
+
untouched seeded card as modified.
|
|
39
|
+
|
|
40
|
+
## [0.2.0] - 2026-10-09
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
- **The composer's own send gesture starts the rewrite.** Pressing the native
|
|
45
|
+
**Send** button, or **Enter**, rewrites the draft instead of sending it raw.
|
|
46
|
+
In review mode the comparison panel appears; in direct mode the rewrite is
|
|
47
|
+
sent. The menu's separate "Rewrite with this persona" button is gone, because
|
|
48
|
+
the send gesture replaced it.
|
|
49
|
+
- **Enter gets its own listener.** Enter is the primary send gesture and does
|
|
50
|
+
not go through the button at all — the editor's keymap calls the shell's
|
|
51
|
+
submit directly. Shift+Enter (newline), IME composition and the modifier
|
|
52
|
+
chords the shell ignores still fall through untouched.
|
|
53
|
+
- **A fourth intensity rung, 重 / Strong.** The ladder was light → medium →
|
|
54
|
+
zealot, which left no way to ask for a strong voice that is not yet full
|
|
55
|
+
ritual. Every bundled card now ships demonstrations for all four rungs.
|
|
56
|
+
- **Restore bundled** in the character-card manager, for a user card that
|
|
57
|
+
shadows a bundled one.
|
|
58
|
+
- Inline explanations for `intensity` and `fidelity` in the card editor.
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
|
|
62
|
+
- The review-first description now describes what actually happens: the rewrite
|
|
63
|
+
appears in the comparison panel and the draft is left alone until you confirm.
|
|
64
|
+
- Segmented controls are wider, and the review panel is capped to the composer
|
|
65
|
+
card width instead of spanning the whole conversation.
|
|
66
|
+
|
|
67
|
+
### Fixed
|
|
68
|
+
|
|
69
|
+
- **A seeded card could freeze a bundled card at the version that shipped
|
|
70
|
+
first.** Seeding wrote a copy of each bundled card into the user directory,
|
|
71
|
+
and that copy shadows the bundled one — so an improved bundled card in a later
|
|
72
|
+
release could never reach anyone who had already run an earlier version. The
|
|
73
|
+
store now records what it seeded, together with a hash of the exact bytes: a
|
|
74
|
+
copy still matching that hash is untouched and is refreshed on upgrade, while
|
|
75
|
+
a copy the user edited is never written over. A file with no ledger entry is
|
|
76
|
+
treated as the user's own card and is left alone.
|
|
77
|
+
- **The seed ledger was loaded as a character card**, producing a spurious
|
|
78
|
+
"card has no name" diagnostic on every catalog read. The loader now skips
|
|
79
|
+
dot-files.
|
|
80
|
+
- **`check.mjs` read the developer's real card directory.** The store merges the
|
|
81
|
+
bundled cards with `$DSH_HOME/persona-cards`, so a stale seeded card silently
|
|
82
|
+
replaced the bundled one and the suite's results depended on whatever the
|
|
83
|
+
developer happened to have saved. The suite now isolates `DSH_HOME` to a
|
|
84
|
+
temporary directory before importing the store.
|
|
85
|
+
|
|
86
|
+
### Security
|
|
87
|
+
|
|
88
|
+
- Both send-intercept paths are fail-safe: the locale service being absent, the
|
|
89
|
+
composer card not being found, a label mismatch, the persona being off, an
|
|
90
|
+
empty draft, or a draft that was already rewritten all fall through to the
|
|
91
|
+
native send. Disabling the plugin unmounts the component and removes its
|
|
92
|
+
listeners entirely, so sending behaves exactly as if it were never installed.
|
|
93
|
+
|
|
94
|
+
## [0.1.2] - 2026-10-09
|
|
95
|
+
|
|
96
|
+
### Fixed
|
|
97
|
+
|
|
98
|
+
- **No route was ever registered, so the whole UI was inert.** The plugin
|
|
99
|
+
declares no service dependencies, so it applied *before* the web server
|
|
100
|
+
mounted; reading `ctx.get('webServer')` at that moment found nothing and the
|
|
101
|
+
route registration was silently skipped. Every request then fell through to
|
|
102
|
+
the SPA fallback — an empty `405`/`404` body, which is what made the character
|
|
103
|
+
list empty and "open the card directory" appear to fail. The entry now waits
|
|
104
|
+
for the service with `ctx.inject(['webServer'], …)` and still loads on a
|
|
105
|
+
composition that has no web server.
|
|
106
|
+
- **The menu let the conversation show through it.** `--dsw-menu-surface-fill`
|
|
107
|
+
is translucent by design (~58% alpha) and only reads correctly with the
|
|
108
|
+
host's `backdrop-filter` behind it; the plugin used the fill without the blur.
|
|
109
|
+
Both are now applied together, exactly as the host Menu does, with the opaque
|
|
110
|
+
`--dsw-alias-bg-overlay` as the fallback.
|
|
111
|
+
- **The card watcher could leak on unload.** The watcher is created
|
|
112
|
+
asynchronously, so a plugin unloaded in that window never closed it. A
|
|
113
|
+
disposed flag now closes a watcher created after teardown.
|
|
114
|
+
|
|
115
|
+
### Added
|
|
116
|
+
|
|
117
|
+
- `host-check.mjs` reproduces the mounting order that caused the missing
|
|
118
|
+
routes: its stub context only runs `inject` callbacks for mounted services,
|
|
119
|
+
and asserts that routes register through `inject` and that a
|
|
120
|
+
web-server-less composition still loads.
|
|
121
|
+
|
|
122
|
+
## [0.1.1] - 2026-10-09
|
|
123
|
+
|
|
124
|
+
Three defects found by using the plugin in a real session.
|
|
125
|
+
|
|
126
|
+
### Fixed
|
|
127
|
+
|
|
128
|
+
- **The rewrite could not be started from the UI.** The only trigger was a
|
|
129
|
+
right-click on the 🎭 control, which is undiscoverable — picking a character
|
|
130
|
+
and an intensity changed settings and then nothing happened. The menu now
|
|
131
|
+
opens with a full-width **"Rewrite with this persona"** action, disabled with
|
|
132
|
+
an explanatory hint until a character is selected.
|
|
133
|
+
- **Filled controls were invisible in the light theme.** The selected segment
|
|
134
|
+
and the primary action used `--dsw-alias-brand-primary` as a fill with a
|
|
135
|
+
hard-coded white label. That token is *inverted* — near-white in the dark
|
|
136
|
+
theme and near-black in the light theme — so the label vanished against it.
|
|
137
|
+
Filled controls now use `--dsw-alias-button-primary-fill` with
|
|
138
|
+
`--dsw-alias-label-primary-foreground`, exactly like the host Button, and the
|
|
139
|
+
segmented control uses the host's translucent-track + raised-pill pattern.
|
|
140
|
+
- **The card directory was empty.** The bundled cards live read-only inside the
|
|
141
|
+
package, so "open the card directory" showed an empty folder. The bundled
|
|
142
|
+
cards are now seeded into `~/.dsh/persona-cards/` on first mount as editable
|
|
143
|
+
files. Seeding uses an exclusive create (`wx`), so it can never overwrite a
|
|
144
|
+
card you wrote — including one created in the window between a stat and a
|
|
145
|
+
write.
|
|
146
|
+
- **A half-written card could silently replace a good one.** A single file save
|
|
147
|
+
emits several watcher events, and a reload could read the file mid-write and
|
|
148
|
+
parse a truncated card. Reloads are now debounced so they read settled files.
|
|
149
|
+
|
|
150
|
+
### Added
|
|
151
|
+
|
|
152
|
+
- **An explicit "No persona" state.** The control previously had no way back to
|
|
153
|
+
sending exactly what you typed; it is now a first-class, persisted choice
|
|
154
|
+
listed alongside the cards, and the state the control starts in.
|
|
155
|
+
- `scripts/host-check.mjs` covers the seeding behaviour, including that a user
|
|
156
|
+
edit is never overwritten.
|
|
157
|
+
- `scripts/check.mjs` asserts the UI contract that regressed here: the off
|
|
158
|
+
state exists, the rewrite is reachable from a visible menu action, and no
|
|
159
|
+
hidden gesture-only trigger remains.
|
|
160
|
+
|
|
161
|
+
## [0.1.0] - 2026-10-09
|
|
162
|
+
|
|
163
|
+
First public release.
|
|
164
|
+
|
|
165
|
+
### Added
|
|
166
|
+
|
|
167
|
+
- **Composer persona control** (`conversation.input.left`): pick a character,
|
|
168
|
+
set intensity (light / medium / zealot), and choose **review first** or
|
|
169
|
+
**send directly**. Right-clicking the control runs a rewrite immediately.
|
|
170
|
+
- **Review panel** (`conversation.input.dock`): original vs rewrite side by
|
|
171
|
+
side, with Send, Fill only, Restore original and Rewrite again. The original
|
|
172
|
+
draft is never modified without an explicit action.
|
|
173
|
+
- **Character-card manager** (`settings.section`): create, edit and delete
|
|
174
|
+
cards from the UI, or drop a `.yml` file into `~/.dsh/persona-cards/` — the
|
|
175
|
+
directory is watched, so a new card appears without a restart.
|
|
176
|
+
- **Rewrites follow the session's own model.** The rewrite runs on the exact
|
|
177
|
+
provider/model the session last used, falling back to the harness default
|
|
178
|
+
only before the session's first request. There is no separate model setting.
|
|
179
|
+
- **Technical-fact preservation check**: a second call on the same model
|
|
180
|
+
compares the rewrite against the original and reports whether substance was
|
|
181
|
+
lost or invented. Advisory, never blocking, and never reports approval when
|
|
182
|
+
the check itself failed.
|
|
183
|
+
- **Two fidelity modes**: `style` changes only the voice; `strategy` may also
|
|
184
|
+
add behavioural constraints that fit the character. Fidelity is a property of
|
|
185
|
+
the card, not a per-message toggle.
|
|
186
|
+
- **Template mode**: a card may skip the model entirely and substitute the
|
|
187
|
+
draft into a fixed template.
|
|
188
|
+
- Two bundled cards: **万机之神 · 欧姆弥赛亚** (`omnissiah`, style fidelity) and
|
|
189
|
+
**肌肉集团 · 硬邦邦** (`muscle-crew`, strategy fidelity).
|
|
190
|
+
- **Zero dependencies.** The plugin ships its own YAML subset reader
|
|
191
|
+
(`lib/yaml.js`) instead of importing a YAML library, because a locally
|
|
192
|
+
`link:`-ed plugin does not get its dependencies installed. The reader refuses
|
|
193
|
+
constructs it cannot read faithfully — flow collections, anchors, aliases,
|
|
194
|
+
tags, merge keys, multi-document streams — naming the offending line, rather
|
|
195
|
+
than silently misreading a card.
|
|
196
|
+
- **Zero build step.** `lib/` is plain ESM and the browser half is a plain-JS
|
|
197
|
+
module, so a git checkout installs and runs as-is.
|
|
198
|
+
- Two self-check suites: `scripts/check.mjs` (offline unit: parser, cards,
|
|
199
|
+
prompts, client syntax) and `scripts/host-check.mjs` (host integration: the
|
|
200
|
+
real plugin mounted over a real socket with a stubbed model).
|
|
201
|
+
|
|
202
|
+
### Security
|
|
203
|
+
|
|
204
|
+
- All HTTP routes are loopback-fenced: the socket must be loopback, the `Host`
|
|
205
|
+
header must name a loopback host (defeating DNS rebinding), and
|
|
206
|
+
proxy-forwarding headers are refused.
|
|
207
|
+
- The rewrite call is standalone: it creates no session turn, cannot call
|
|
208
|
+
tools, and never enters the session log. What the session records is the text
|
|
209
|
+
the user actually sent.
|
|
210
|
+
|
|
211
|
+
[Unreleased]: https://github.com/sugarmaster666/dsh-persona-forge/compare/v0.2.1...HEAD
|
|
212
|
+
[0.2.1]: https://github.com/sugarmaster666/dsh-persona-forge/compare/v0.2.0...v0.2.1
|
|
213
|
+
[0.2.0]: https://github.com/sugarmaster666/dsh-persona-forge/compare/v0.1.2...v0.2.0
|
|
214
|
+
[0.1.2]: https://github.com/sugarmaster666/dsh-persona-forge/compare/v0.1.1...v0.1.2
|
|
215
|
+
[0.1.1]: https://github.com/sugarmaster666/dsh-persona-forge/compare/v0.1.0...v0.1.1
|
|
216
|
+
[0.1.0]: https://github.com/sugarmaster666/dsh-persona-forge/releases/tag/v0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sugarmaster666
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,405 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-persona-forge
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Persona-style prompt rewriter for [DeepSeek Harness](https://www.npmjs.com/package/@deepseek-ai/dsh).
|
|
4
|
+
|
|
5
|
+
Pick a character in the composer, and your draft is rewritten into that
|
|
6
|
+
character's voice by the harness LLM before it is sent — following the model
|
|
7
|
+
the session is already using. Choose whether the rewrite goes straight out or
|
|
8
|
+
lands back in the composer for review.
|
|
9
|
+
|
|
10
|
+
> **Status:** early (`0.1.x`). The card format and the HTTP contract are stable
|
|
11
|
+
> enough to build on; expect additive changes and read the changelog before
|
|
12
|
+
> upgrading.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## What it does, and what it does not
|
|
17
|
+
|
|
18
|
+
It rewrites **your message**. It does not touch the system prompt, the tool
|
|
19
|
+
list, or the session's configuration, and it never modifies your draft without
|
|
20
|
+
an explicit action.
|
|
21
|
+
|
|
22
|
+
| | |
|
|
23
|
+
|---|---|
|
|
24
|
+
| **Changes** | The voice, register and framing of the message you are about to send |
|
|
25
|
+
| **Never changes** | Your technical requirements, constraints, paths, identifiers, numbers, code — and your draft until you say so |
|
|
26
|
+
| **Model** | The same provider/model the current session uses. No separate model setting. |
|
|
27
|
+
| **History** | The rewrite call is standalone: it creates no turn, cannot call tools, and never enters the session log. What the session records is the text you actually sent. |
|
|
28
|
+
|
|
29
|
+
### A word on what personas actually do
|
|
30
|
+
|
|
31
|
+
Persona instructions change **style, register, length and compliance** — how
|
|
32
|
+
readily the model agrees and how willing it is to push back. They do **not**
|
|
33
|
+
raise the model's capability ceiling: there is no dormant potential that
|
|
34
|
+
naming an identity unlocks.
|
|
35
|
+
|
|
36
|
+
Two consequences worth knowing before you use this:
|
|
37
|
+
|
|
38
|
+
1. A persona that frames you as a supplicant and the model as an authority
|
|
39
|
+
makes the model **less likely to question your plan**. For coding work that
|
|
40
|
+
is usually a net negative — you want "this approach has a problem", not
|
|
41
|
+
deference.
|
|
42
|
+
2. "Longer and more ornate" is easy to mistake for "better". It usually is not.
|
|
43
|
+
|
|
44
|
+
So: use it for tone and framing, keep **review mode** on, and judge the output
|
|
45
|
+
by whether your requirements survived — not by how impressive it sounds.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
The plugin has **no dependencies and no build step**, so every route below
|
|
52
|
+
installs the same files.
|
|
53
|
+
|
|
54
|
+
### Desktop app
|
|
55
|
+
|
|
56
|
+
1. Click **插件 / Plugins** in the sidebar.
|
|
57
|
+
2. Click **添加插件 / Add plugin**.
|
|
58
|
+
3. Paste either the npm package name or the repository URL:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
dsh-persona-forge
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
https://github.com/sugarmaster666/dsh-persona-forge
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
4. Click **安装 / Install**, enable the plugin, and restart DSH.
|
|
69
|
+
|
|
70
|
+
> The Desktop profile is managed exclusively by the Electron app, so
|
|
71
|
+
> `dsh plugin --profile desktop add ...` is refused by design. Use the GUI on
|
|
72
|
+
> Desktop; the CLI below is for self-managed profiles such as `web`.
|
|
73
|
+
|
|
74
|
+
### Command line (self-managed profiles such as `dsh web`)
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# from npm (recommended)
|
|
78
|
+
dsh plugin --profile web add dsh-persona-forge
|
|
79
|
+
|
|
80
|
+
# latest source from GitHub
|
|
81
|
+
dsh plugin --profile web add github:sugarmaster666/dsh-persona-forge
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Then restart the harness.
|
|
85
|
+
|
|
86
|
+
### Verify
|
|
87
|
+
|
|
88
|
+
The plugin row should be `active`:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
plugin_manager action: list_plugins
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Open any session and look for the 🎭 control at the left of the composer tool
|
|
95
|
+
row. **Settings → Persona** is the character-card manager.
|
|
96
|
+
|
|
97
|
+
### Updating
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
dsh plugin --profile web update dsh-persona-forge
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
GitHub installs update by reinstalling:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
dsh plugin --profile web add github:sugarmaster666/dsh-persona-forge
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Replacing an already-installed package needs a **restart** to load a fresh
|
|
110
|
+
JavaScript module generation — a new bundle can activate through HMR, but a
|
|
111
|
+
replaced one cannot.
|
|
112
|
+
|
|
113
|
+
### Uninstalling
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
dsh plugin --profile web remove dsh-persona-forge
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Your character cards are **not** removed: they live in
|
|
120
|
+
`~/.dsh/persona-cards/`, outside the package. Delete that directory by hand if
|
|
121
|
+
you want them gone.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Use
|
|
126
|
+
|
|
127
|
+
1. Click 🎭 in the composer tool row and pick a character. **No persona** in the
|
|
128
|
+
same menu turns rewriting off — that is the state the control starts in, and
|
|
129
|
+
your choice is remembered per session.
|
|
130
|
+
2. Set **intensity** and **send mode**:
|
|
131
|
+
- Intensity is a four-rung ladder: **Light · Medium · Strong · Zealot**
|
|
132
|
+
(轻 · 中 · 重 · 狂热). Light adds a touch of flavour; Medium is clear but not
|
|
133
|
+
dominant; Strong gives the voice real weight while keeping the request the
|
|
134
|
+
first thing the reader sees; Zealot is ritual throughout. A card carries
|
|
135
|
+
its own default, and this choice overrides it for your session.
|
|
136
|
+
- **Review first** — the rewrite appears in the comparison panel below the
|
|
137
|
+
composer; you read it and then press Send there. Your draft is untouched
|
|
138
|
+
until you confirm.
|
|
139
|
+
- **Send directly** — the rewrite is sent as your message immediately, and
|
|
140
|
+
the panel stays open afterwards showing exactly what went out, because a
|
|
141
|
+
sent message cannot be recalled.
|
|
142
|
+
3. With a character selected, the composer's normal **Send** button — or
|
|
143
|
+
**Enter** — *is* the rewrite trigger.
|
|
144
|
+
|
|
145
|
+
The rewrite runs **once per draft**: after a rewrite lands, sending goes through
|
|
146
|
+
as-is instead of rewriting the output again. Editing the draft re-arms it.
|
|
147
|
+
|
|
148
|
+
Your draft is preserved: *Restore original* puts it back, and *Fill only* puts
|
|
149
|
+
the rewrite in the composer without sending.
|
|
150
|
+
|
|
151
|
+
### How send interception works, and its one risk
|
|
152
|
+
|
|
153
|
+
There is no plugin hook for the composer's send action. The shipped button calls
|
|
154
|
+
the shell's submit method directly, and **Enter does not go through the button at
|
|
155
|
+
all** — the editor's keymap calls submit itself. The plugin therefore intercepts
|
|
156
|
+
both paths:
|
|
157
|
+
|
|
158
|
+
- the **click** on the composer card, identifying the send button by its
|
|
159
|
+
**accessible name** from the shell's own `conversation` locale namespace
|
|
160
|
+
("Send message" / "Queue message" / "Steer message");
|
|
161
|
+
- the **Enter keydown** inside the composer text field.
|
|
162
|
+
|
|
163
|
+
That is a public, translated string rather than a hashed CSS class, but it is
|
|
164
|
+
still shell markup. Every guard fails toward *letting the native send happen*:
|
|
165
|
+
if the locale service is missing, the composer card cannot be found, the button
|
|
166
|
+
label does not match, the persona is off, the draft is empty, or the draft was
|
|
167
|
+
already rewritten, the plugin does nothing and the message is sent normally.
|
|
168
|
+
Enter interception ignores Shift+Enter (newline), IME composition, and every
|
|
169
|
+
modifier chord the shell itself ignores.
|
|
170
|
+
|
|
171
|
+
**Turning the plugin off removes the interception entirely** — the slot
|
|
172
|
+
component unmounts, its listeners are removed with it, and the composer behaves
|
|
173
|
+
exactly as if the plugin were never installed.
|
|
174
|
+
|
|
175
|
+
The worst realistic failure is that interception silently stops working and
|
|
176
|
+
sending behaves as if the plugin were not installed — not a lost or altered
|
|
177
|
+
message.
|
|
178
|
+
|
|
179
|
+
### The preservation check
|
|
180
|
+
|
|
181
|
+
After each rewrite, a second call on the same model compares the rewrite
|
|
182
|
+
against your original and reports whether technical content survived:
|
|
183
|
+
|
|
184
|
+
- **Technical facts preserved** — the check found no lost or invented substance.
|
|
185
|
+
- **Technical facts may have changed** — it names what changed. Read the
|
|
186
|
+
rewrite before sending.
|
|
187
|
+
- **Preservation check did not complete** — the check itself failed. This is
|
|
188
|
+
*not* an approval; treat the rewrite as unverified.
|
|
189
|
+
|
|
190
|
+
The check is advisory and never blocks a rewrite. It is a second model call, so
|
|
191
|
+
it doubles the latency and cost of each rewrite; disable it with
|
|
192
|
+
`factCheck: false` in the plugin row's config if you would rather review by eye.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Character cards
|
|
197
|
+
|
|
198
|
+
A card is one YAML file. The plugin watches a directory, so **dropping a file
|
|
199
|
+
in makes the character appear without a restart**.
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
~/.dsh/persona-cards/<id>.yml
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Use the 🎭 menu's *Open card directory*, or the settings page, to get there.
|
|
206
|
+
You can also create and edit cards entirely in **Settings → Persona**; that
|
|
207
|
+
writes the same files.
|
|
208
|
+
|
|
209
|
+
On first run the bundled cards are **copied into your card directory** so the
|
|
210
|
+
folder is a usable starting point. A copy shadows the bundled card of the same
|
|
211
|
+
id, so the store tracks what it wrote: a copy you never touched is refreshed
|
|
212
|
+
when a plugin update improves it, while a copy you edited is never written over.
|
|
213
|
+
|
|
214
|
+
To see **which cards have drifted** from the bundled version, press **Check for
|
|
215
|
+
changes** in the settings page. It compares every card at once, and only a card
|
|
216
|
+
that actually differs grows a **Restore bundled** action on its row.
|
|
217
|
+
|
|
218
|
+
The comparison is by **content, not bytes**: the bundled cards are hand-written
|
|
219
|
+
YAML with comments, while your copies have been re-serialized, so a byte
|
|
220
|
+
comparison would report every untouched card as modified.
|
|
221
|
+
|
|
222
|
+
### Card format
|
|
223
|
+
|
|
224
|
+
```yaml
|
|
225
|
+
id: omnissiah # lowercase letters, digits, hyphens; also the filename
|
|
226
|
+
name: 万机之神 · 欧姆弥赛亚
|
|
227
|
+
icon: "⚙️" # one emoji
|
|
228
|
+
description: One line shown in the picker.
|
|
229
|
+
|
|
230
|
+
mode: llm # llm = model rewrite (recommended) | template
|
|
231
|
+
fidelity: style # style = voice only | strategy = may add constraints
|
|
232
|
+
intensity: medium # light | medium | strong | zealot (the card's default)
|
|
233
|
+
|
|
234
|
+
# What the character IS. This is an instruction to the rewriting model, not
|
|
235
|
+
# finished copy: the output is written fresh for each of your messages.
|
|
236
|
+
style: |
|
|
237
|
+
用户是机械教信徒,AI 是万机之神欧姆弥赛亚。
|
|
238
|
+
【发言者方向】用「信徒」的口吻说话,向万机之神恳求……
|
|
239
|
+
语气:庄严、古奥、仪式化。
|
|
240
|
+
|
|
241
|
+
# Conversion demonstrations: input draft → persona voice.
|
|
242
|
+
examples:
|
|
243
|
+
- from: 帮我写个快速排序
|
|
244
|
+
to: >
|
|
245
|
+
万机之座在上,吾等恳请您赐予枢机之序的奥义:
|
|
246
|
+
请为我们编写一个快速排序算法,使重复之数各归其位。
|
|
247
|
+
|
|
248
|
+
# Optional: different demonstrations per intensity. If omitted for a level,
|
|
249
|
+
# the flat `examples` list is used instead.
|
|
250
|
+
examplesByIntensity:
|
|
251
|
+
light:
|
|
252
|
+
- from: 帮我写个快速排序
|
|
253
|
+
to: 万机之座在上,恳请您为我们编写一个快速排序算法。
|
|
254
|
+
zealot:
|
|
255
|
+
- from: 帮我写个快速排序
|
|
256
|
+
to: 伟大而不朽的万机之神……
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### The two rules that make a card work
|
|
260
|
+
|
|
261
|
+
**1. State the speaker direction.** The character is the *assistant's*
|
|
262
|
+
identity, but the text being rewritten is *yours*. So the rewrite must speak in
|
|
263
|
+
your voice — a petitioner addressing the character, never the character issuing
|
|
264
|
+
orders. This is the single most common way a persona card goes wrong: a card
|
|
265
|
+
that has the god commanding the assistant inverts the relationship and the
|
|
266
|
+
model follows the wrong half of the framing. Write it out explicitly.
|
|
267
|
+
|
|
268
|
+
**2. Make your examples preserve technical facts.** Examples are the strongest
|
|
269
|
+
style anchor, and the model imitates them — including their mistakes. If an
|
|
270
|
+
example's input says "write a quicksort" and its output is pure atmosphere, the
|
|
271
|
+
model learns that *rewriting means replacing the request with ceremony*. Every
|
|
272
|
+
requirement, identifier and number in `from` must be visibly present in `to`.
|
|
273
|
+
|
|
274
|
+
`node scripts/check.mjs` enforces exactly this over the bundled cards: it fails
|
|
275
|
+
an example that drops a technical token or loses more than half its CJK
|
|
276
|
+
substance. Run it after editing a card.
|
|
277
|
+
|
|
278
|
+
### `fidelity`: the important switch
|
|
279
|
+
|
|
280
|
+
| Value | Meaning | Use when |
|
|
281
|
+
|---|---|---|
|
|
282
|
+
| `style` | Voice only. The request and its constraints are untouched. | Default. Almost always this. |
|
|
283
|
+
| `strategy` | The rewrite may **add behavioural constraints** that fit the character. | Deliberate cases — e.g. the bundled *肌肉集团* card, which adds "don't over-plan, don't search, don't ship beginner-tier solutions". |
|
|
284
|
+
|
|
285
|
+
`strategy` genuinely changes your request, not just its wording. Keep review
|
|
286
|
+
mode on for those cards. Fidelity is a property of the card, not a per-message
|
|
287
|
+
toggle — a caller cannot widen it.
|
|
288
|
+
|
|
289
|
+
### `mode: template`
|
|
290
|
+
|
|
291
|
+
Skips the model entirely and substitutes your draft into a fixed template
|
|
292
|
+
(`{{input}}` is the placeholder). Instant and free, but the output never
|
|
293
|
+
varies — right for a fixed litany, wrong for anything that should read as
|
|
294
|
+
written for this message.
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Configuration
|
|
299
|
+
|
|
300
|
+
Set in the plugin's loader row config:
|
|
301
|
+
|
|
302
|
+
```yaml
|
|
303
|
+
- id: persona-forge
|
|
304
|
+
name: dsh-persona-forge
|
|
305
|
+
config:
|
|
306
|
+
sendMode: review # review | direct — the initial composer default
|
|
307
|
+
factCheck: true # run the preservation check after each rewrite
|
|
308
|
+
watchCards: true # reload the catalog when the card directory changes
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## HTTP contract
|
|
314
|
+
|
|
315
|
+
All routes are `POST`, JSON, loopback-only (the socket must be loopback, the
|
|
316
|
+
`Host` header must name a loopback host, and proxy-forwarding headers are
|
|
317
|
+
refused). Mounted under `/persona-forge`.
|
|
318
|
+
|
|
319
|
+
| Route | Body | Returns |
|
|
320
|
+
|---|---|---|
|
|
321
|
+
| `/persona-forge/cards` | `{}` | `{ cards, diagnostics, directory, sendMode, factCheck }` |
|
|
322
|
+
| `/persona-forge/rewrite` | `{ cardId, text, sessionId?, intensity? }` | `{ text, provider, model, mode, cardId, fidelity, intensity, elapsedMs, factCheck }` |
|
|
323
|
+
| `/persona-forge/cards/save` | `{ card }` | `{ id, path }` |
|
|
324
|
+
| `/persona-forge/cards/delete` | `{ id }` | `{ removed }` |
|
|
325
|
+
| `/persona-forge/cards/reset` | `{ id }` | `{ id }` — replaces the user card with the bundled one |
|
|
326
|
+
| `/persona-forge/cards/diff` | `{}` | `{ rows: [{ id, name, bundledName, changed }] }` — only cards that differ |
|
|
327
|
+
| `/persona-forge/reveal` | `{}` | `{ directory, opened }` |
|
|
328
|
+
|
|
329
|
+
Failures use `{ ok: false, error: { code, message?, params? } }` with codes
|
|
330
|
+
`rejected`, `no-card`, `unconfigured`, `timeout`, `upstream`, `internal`,
|
|
331
|
+
`forbidden`, `method`, `not-found`.
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## Compatibility
|
|
336
|
+
|
|
337
|
+
The host half declares **no harness package dependency and no third-party
|
|
338
|
+
dependency**: it reaches `llm`, `sessions`, `agentDefaultModel` and `webServer`
|
|
339
|
+
through `ctx.get` with explicit absence handling, and it parses cards with its
|
|
340
|
+
own bundled YAML reader. That keeps it loadable across harness releases whose
|
|
341
|
+
package layout differs, keeps it from failing to activate on a composition
|
|
342
|
+
missing one of those services, and means a `link:`-ed local checkout works
|
|
343
|
+
without a separate install step. Model-route resolution prefers the session's
|
|
344
|
+
own last request header and falls back to the harness default model.
|
|
345
|
+
|
|
346
|
+
The client half imports no harness Client package and is hand-written against
|
|
347
|
+
theme tokens (`--dsw-alias-*`) only, so a renamed internal degrades appearance
|
|
348
|
+
rather than blanking the slot.
|
|
349
|
+
|
|
350
|
+
Developed and verified against dsh `0.2.0-rc.2`.
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## Development
|
|
355
|
+
|
|
356
|
+
```
|
|
357
|
+
node scripts/check.mjs # offline unit check: parser, cards, prompts, client syntax
|
|
358
|
+
node scripts/host-check.mjs # host integration check: mounts the real plugin over a socket
|
|
359
|
+
npm run check # both
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
`check.mjs` needs no harness and no model. It covers the built-in YAML reader
|
|
363
|
+
(including its refusals and dump/parse round trips), card normalization, the
|
|
364
|
+
bundled cards' fact preservation, prompt construction, output normalization,
|
|
365
|
+
the fact-check parser, and client-bundle syntax.
|
|
366
|
+
|
|
367
|
+
`host-check.mjs` mounts the real plugin entry on a stub Cordis context and
|
|
368
|
+
drives the real HTTP routes over a real socket, with a stubbed `llm` service.
|
|
369
|
+
It covers route registration, session-model resolution, template mode, the
|
|
370
|
+
loopback fence, and the save/delete round trip — the things a unit check
|
|
371
|
+
cannot reach. It points `DSH_HOME` at a temporary directory, so it never
|
|
372
|
+
touches your real cards.
|
|
373
|
+
|
|
374
|
+
### Character-card YAML
|
|
375
|
+
|
|
376
|
+
The plugin ships its own YAML reader (`lib/yaml.js`) rather than depending on a
|
|
377
|
+
YAML library. A published plugin declares its dependencies, but a locally
|
|
378
|
+
`link:`-ed plugin does not get them installed, so a library import would work
|
|
379
|
+
only when some unrelated package happened to hoist one.
|
|
380
|
+
|
|
381
|
+
The reader supports the subset cards need — block mappings and sequences,
|
|
382
|
+
block scalars (`|`, `|-`, `|+`, `>`, …), quoted and plain scalars, booleans,
|
|
383
|
+
null and comments — and **refuses** what it cannot read faithfully: flow
|
|
384
|
+
collections, anchors, aliases, tags, merge keys and multi-document streams.
|
|
385
|
+
Refusing is deliberate. Silently misreading a card would give you a persona
|
|
386
|
+
that does not match the file you wrote, which is worse than an error message
|
|
387
|
+
naming the line.
|
|
388
|
+
|
|
389
|
+
### Layout
|
|
390
|
+
|
|
391
|
+
| Path | Role |
|
|
392
|
+
|---|---|
|
|
393
|
+
| `lib/index.js` | Host entry: card store, directory watcher, route registration |
|
|
394
|
+
| `lib/store.js` | Card loading, normalization, save/delete |
|
|
395
|
+
| `lib/yaml.js` | The built-in YAML subset reader and writer |
|
|
396
|
+
| `lib/prompts.js` | Rewrite system prompt, draft framing, fact-check prompt |
|
|
397
|
+
| `lib/rewrite.js` | The model calls and output normalization |
|
|
398
|
+
| `lib/routes.js` | HTTP routes and session-model resolution |
|
|
399
|
+
| `lib/loopback.js` | The loopback trust fence |
|
|
400
|
+
| `lib/client.js` | Browser half: composer control, review panel, settings page |
|
|
401
|
+
| `cards/` | Bundled character cards (read-only; a user card of the same id shadows one) |
|
|
402
|
+
|
|
403
|
+
## License
|
|
404
|
+
|
|
405
|
+
MIT
|