dsh-persona-forge 0.2.1 → 0.3.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 +120 -2
- package/README.en.md +518 -0
- package/README.md +200 -248
- package/cards/muscle-crew.yml +3 -3
- package/lib/client.js +490 -42
- package/lib/index.js +16 -3
- package/lib/preserve.js +138 -0
- package/lib/prompts.js +164 -3
- package/lib/routes.js +169 -9
- package/lib/shared/protocol.js +10 -0
- package/lib/store.js +24 -2
- package/package.json +3 -3
- package/README.zh-CN.md +0 -297
package/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,124 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## [0.3.0] - 2026-10-09
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **AI 代填(AI draft)**, in the character-card manager. Describe the character
|
|
14
|
+
in a sentence — "a grumpy but utterly reliable old lighthouse keeper" — and the
|
|
15
|
+
model writes the whole card: the style text, a name, an icon, a description and
|
|
16
|
+
the conversion examples for all four intensity levels.
|
|
17
|
+
|
|
18
|
+
The result **only prefills the form**. Nothing is written to the card directory
|
|
19
|
+
until the user reviews it and presses Save, so a generation the user dislikes
|
|
20
|
+
costs one reload — the same review discipline the rewrite panel enforces. The
|
|
21
|
+
generated card is also passed through the very same validator the save path
|
|
22
|
+
uses, so this route cannot hand the form a card that cannot be saved.
|
|
23
|
+
|
|
24
|
+
It sits **inside the new-card flow** (the form that "New character card" opens),
|
|
25
|
+
with a hint line on the settings page so it is discoverable without pressing a
|
|
26
|
+
button. It uses the **harness default model**, and says so: the
|
|
27
|
+
`settings.section` slot receives only `{ close }` from the shell, so it has no
|
|
28
|
+
session id to follow. The resolved model is displayed before and after
|
|
29
|
+
generating, rather than leaving the user to assume it matches the session.
|
|
30
|
+
|
|
31
|
+
- A **free, local preservation check** (`lib/preserve.js`), always on. It compares
|
|
32
|
+
the technical tokens of the draft against the rewrite — identifiers, paths,
|
|
33
|
+
versions, numbers, `--flags`, acronyms — and **names** what went missing. It
|
|
34
|
+
makes no model call, so it costs no tokens and adds no latency.
|
|
35
|
+
|
|
36
|
+
- **Per-message rewrite mode and fidelity** in the 🎭 menu, alongside intensity.
|
|
37
|
+
Mode falls back to the card's own when the card has no content for the chosen
|
|
38
|
+
mode. Fidelity may be **narrowed** (a `strategy` card asked to behave as
|
|
39
|
+
voice-only for one message) but never **widened**, and the server enforces the
|
|
40
|
+
same rule.
|
|
41
|
+
|
|
42
|
+
- A **model-check switch** in the 🎭 menu, remembered per session.
|
|
43
|
+
|
|
44
|
+
- The card form now edits the **per-intensity examples**. They were part of the
|
|
45
|
+
card but invisible in the UI, which is what made the bug below possible.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- **The composer menu is a picker again, not a settings form.** It had grown to
|
|
50
|
+
five equal-weight segmented rows plus 248 characters of permanently visible
|
|
51
|
+
explanation, so picking a character meant scanning past four configuration
|
|
52
|
+
rows every time. It is now two layers: card list, intensity and send mode
|
|
53
|
+
always visible; **rewrite mode and fidelity** — per-session overrides of a
|
|
54
|
+
card's own declaration — folded behind "More settings", shut by default. The
|
|
55
|
+
collapsed row still names anything overridden, so a hidden choice cannot
|
|
56
|
+
surprise the user later. Measured: 5 rows + 248 hint chars → 2 rows + 0
|
|
57
|
+
permanent hint chars.
|
|
58
|
+
|
|
59
|
+
- **Fidelity is only offered when the card may add behavioural constraints.** On
|
|
60
|
+
a voice-only card both options resolve identically, so the row was a control
|
|
61
|
+
that could not do anything, paired with a paragraph explaining why the thing
|
|
62
|
+
you would want was refused. It is now absent on `style` cards.
|
|
63
|
+
|
|
64
|
+
- **The model check moved to the settings page** as a global preference rather
|
|
65
|
+
than a per-session switch in the composer menu. It answers "do I want to pay
|
|
66
|
+
for the second call", which is decided once, not re-asked on the way to every
|
|
67
|
+
send.
|
|
68
|
+
|
|
69
|
+
- "No persona" moved to the end of the card list: it is an exit action, and
|
|
70
|
+
listing it first made it the first thing read in a menu whose point is choosing
|
|
71
|
+
a character.
|
|
72
|
+
|
|
73
|
+
- **The model-based preservation check is now off by default** and switchable
|
|
74
|
+
from the settings page. It is the only thing that can see a requirement
|
|
75
|
+
reworded weaker, or a constraint added in prose — but it costs a second model
|
|
76
|
+
call on **every** send, and the new local check already proves the common and
|
|
77
|
+
most damaging failure for free. `factCheck` in the plugin config now sets that
|
|
78
|
+
default, and only an explicit `true` enables it.
|
|
79
|
+
|
|
80
|
+
- The Chinese README is the primary `README.md` and the English one is
|
|
81
|
+
`README.en.md`, so GitHub, npm and the repository landing page open in Chinese,
|
|
82
|
+
with a language switch at the top of each file. The npm package description is
|
|
83
|
+
Chinese for the same reason.
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
|
|
87
|
+
- **Editing a card silently destroyed its per-intensity examples.** The form did
|
|
88
|
+
not carry `examplesByIntensity`, so pressing Save on any card rebuilt it
|
|
89
|
+
without the four demonstration sets and the card fell back to its flat
|
|
90
|
+
`examples` list. Renaming a bundled card was enough to lose all four intensity
|
|
91
|
+
sets. The form now shows and round-trips them.
|
|
92
|
+
|
|
93
|
+
- **Every bundled card was labelled 自定义 (custom).** Bundled cards are seeded
|
|
94
|
+
into the user card directory as editable starting points, so the tag was
|
|
95
|
+
derived from "does a user file exist for this id" — true for cards nobody had
|
|
96
|
+
ever touched. The row tag now reports real provenance, with a new **已修改
|
|
97
|
+
(edited)** state for a bundled card the user has changed, and it is computed
|
|
98
|
+
with the same semantic comparison the drift check uses, so the tag and the
|
|
99
|
+
check's verdict now agree **by construction** instead of contradicting each
|
|
100
|
+
other on the same screen.
|
|
101
|
+
|
|
102
|
+
- **Delete was offered where it could not take effect.** A card whose content
|
|
103
|
+
still matches the bundle has no user-owned file: removing it cleared a seeded
|
|
104
|
+
copy that the next start wrote straight back, so the button reported success
|
|
105
|
+
and changed nothing. Delete is now shown only where it does something, and the
|
|
106
|
+
confirmation for an edited bundled card says that its changes are discarded
|
|
107
|
+
and the bundled version returns.
|
|
108
|
+
|
|
109
|
+
- **A cancelled rewrite could not be retried.** `lastRewrite` was written on
|
|
110
|
+
every successful rewrite and never removed, so that the send gesture could
|
|
111
|
+
tell "the rewrite is already in the composer" from "not rewritten yet".
|
|
112
|
+
Restoring the original put the draft text back but left that memory in place,
|
|
113
|
+
so the next send matched "already rewritten" and the plugin silently let the
|
|
114
|
+
raw text through. Restoring is a rewind, not a reviewed result, so it now
|
|
115
|
+
deletes the entry — conditionally on putting back the original, because
|
|
116
|
+
sending the rewrite itself must still count as already rewritten.
|
|
117
|
+
|
|
118
|
+
- **A narrowed fidelity never reached the model.** The route computed the
|
|
119
|
+
narrowed value and reported it in the response, but `buildSystemPrompt` read
|
|
120
|
+
`card.fidelity` directly — so the setting appeared to work and did nothing. The
|
|
121
|
+
effective fidelity is now passed into the prompt builder, and a test asserts on
|
|
122
|
+
the prompt itself rather than only on the response.
|
|
123
|
+
|
|
124
|
+
- Three examples in the bundled 肌肉集团 · 硬邦邦 card dropped the word "bug"
|
|
125
|
+
from their input. The self-check now verifies the per-intensity examples too,
|
|
126
|
+
not just the flat list, which is what surfaced them.
|
|
10
127
|
|
|
11
128
|
## [0.2.1] - 2026-10-09
|
|
12
129
|
|
|
@@ -208,7 +325,8 @@ First public release.
|
|
|
208
325
|
tools, and never enters the session log. What the session records is the text
|
|
209
326
|
the user actually sent.
|
|
210
327
|
|
|
211
|
-
[Unreleased]: https://github.com/sugarmaster666/dsh-persona-forge/compare/v0.
|
|
328
|
+
[Unreleased]: https://github.com/sugarmaster666/dsh-persona-forge/compare/v0.3.0...HEAD
|
|
329
|
+
[0.3.0]: https://github.com/sugarmaster666/dsh-persona-forge/compare/v0.2.1...v0.3.0
|
|
212
330
|
[0.2.1]: https://github.com/sugarmaster666/dsh-persona-forge/compare/v0.2.0...v0.2.1
|
|
213
331
|
[0.2.0]: https://github.com/sugarmaster666/dsh-persona-forge/compare/v0.1.2...v0.2.0
|
|
214
332
|
[0.1.2]: https://github.com/sugarmaster666/dsh-persona-forge/compare/v0.1.1...v0.1.2
|
package/README.en.md
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
# dsh-persona-forge
|
|
2
|
+
|
|
3
|
+
English | [中文](README.md)
|
|
4
|
+
|
|
5
|
+
Persona-style prompt rewriter for [DeepSeek Harness](https://www.npmjs.com/package/@deepseek-ai/dsh).
|
|
6
|
+
|
|
7
|
+
Pick a character in the composer, and your draft is rewritten into that
|
|
8
|
+
character's voice by the harness LLM before it is sent — following the model
|
|
9
|
+
the session is already using. Choose whether the rewrite goes straight out or
|
|
10
|
+
lands back in the composer for review.
|
|
11
|
+
|
|
12
|
+
> **Status:** early (`0.3.x`). The card format and the HTTP contract are stable
|
|
13
|
+
> enough to build on; expect additive changes and read the changelog before
|
|
14
|
+
> upgrading.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## What it does, and what it does not
|
|
19
|
+
|
|
20
|
+
It rewrites **your message**. It does not touch the system prompt, the tool
|
|
21
|
+
list, or the session's configuration, and it never modifies your draft without
|
|
22
|
+
an explicit action.
|
|
23
|
+
|
|
24
|
+
| | |
|
|
25
|
+
|---|---|
|
|
26
|
+
| **Changes** | The voice, register and framing of the message you are about to send |
|
|
27
|
+
| **Never changes** | Your technical requirements, constraints, paths, identifiers, numbers, code — and your draft until you say so |
|
|
28
|
+
| **Model** | The rewrite uses the same provider/model the current session uses — no separate model setting. (The settings page's AI card draft instead uses the harness default, because that slot gets no session id; it shows you which model it will use.) |
|
|
29
|
+
| **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. |
|
|
30
|
+
|
|
31
|
+
### A word on what personas actually do
|
|
32
|
+
|
|
33
|
+
Persona instructions change **style, register, length and compliance** — how
|
|
34
|
+
readily the model agrees and how willing it is to push back. They do **not**
|
|
35
|
+
raise the model's capability ceiling: there is no dormant potential that
|
|
36
|
+
naming an identity unlocks.
|
|
37
|
+
|
|
38
|
+
Two consequences worth knowing before you use this:
|
|
39
|
+
|
|
40
|
+
1. A persona that frames you as a supplicant and the model as an authority
|
|
41
|
+
makes the model **less likely to question your plan**. For coding work that
|
|
42
|
+
is usually a net negative — you want "this approach has a problem", not
|
|
43
|
+
deference.
|
|
44
|
+
2. "Longer and more ornate" is easy to mistake for "better". It usually is not.
|
|
45
|
+
|
|
46
|
+
So: use it for tone and framing, keep **review mode** on, and judge the output
|
|
47
|
+
by whether your requirements survived — not by how impressive it sounds.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
The plugin has **no dependencies and no build step**, so every route below
|
|
54
|
+
installs the same files.
|
|
55
|
+
|
|
56
|
+
### Desktop app
|
|
57
|
+
|
|
58
|
+
1. Click **插件 / Plugins** in the sidebar.
|
|
59
|
+
2. Click **添加插件 / Add plugin**.
|
|
60
|
+
3. Paste either the npm package name or the repository URL:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
dsh-persona-forge
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
https://github.com/sugarmaster666/dsh-persona-forge
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
4. Click **安装 / Install**, enable the plugin, and restart DSH.
|
|
71
|
+
|
|
72
|
+
> The Desktop profile is managed exclusively by the Electron app, so
|
|
73
|
+
> `dsh plugin --profile desktop add ...` is refused by design. Use the GUI on
|
|
74
|
+
> Desktop; the CLI below is for self-managed profiles such as `web`.
|
|
75
|
+
|
|
76
|
+
### Command line (self-managed profiles such as `dsh web`)
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# from npm (recommended)
|
|
80
|
+
dsh plugin --profile web add dsh-persona-forge
|
|
81
|
+
|
|
82
|
+
# latest source from GitHub
|
|
83
|
+
dsh plugin --profile web add github:sugarmaster666/dsh-persona-forge
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Then restart the harness.
|
|
87
|
+
|
|
88
|
+
### Verify
|
|
89
|
+
|
|
90
|
+
The plugin row should be `active`:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
plugin_manager action: list_plugins
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Open any session and look for the 🎭 control at the left of the composer tool
|
|
97
|
+
row. **Settings → Persona** is the character-card manager.
|
|
98
|
+
|
|
99
|
+
### Updating
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
dsh plugin --profile web update dsh-persona-forge
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
GitHub installs update by reinstalling:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
dsh plugin --profile web add github:sugarmaster666/dsh-persona-forge
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Replacing an already-installed package needs a **restart** to load a fresh
|
|
112
|
+
JavaScript module generation — a new bundle can activate through HMR, but a
|
|
113
|
+
replaced one cannot.
|
|
114
|
+
|
|
115
|
+
### Uninstalling
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
dsh plugin --profile web remove dsh-persona-forge
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Your character cards are **not** removed: they live in
|
|
122
|
+
`~/.dsh/persona-cards/`, outside the package. Delete that directory by hand if
|
|
123
|
+
you want them gone.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Use
|
|
128
|
+
|
|
129
|
+
1. Click 🎭 in the composer tool row and pick a character. **No persona**, at the
|
|
130
|
+
end of the same list, turns rewriting off — that is the state the control
|
|
131
|
+
starts in, and your choice is remembered per session.
|
|
132
|
+
2. Set **intensity** and **send mode**:
|
|
133
|
+
- Intensity is a four-rung ladder: **Light · Medium · Strong · Zealot**
|
|
134
|
+
(轻 · 中 · 重 · 狂热). Light adds a touch of flavour; Medium is clear but not
|
|
135
|
+
dominant; Strong gives the voice real weight while keeping the request the
|
|
136
|
+
first thing the reader sees; Zealot is ritual throughout. A card carries
|
|
137
|
+
its own default, and this choice overrides it for your session.
|
|
138
|
+
- **Review first** — the rewrite appears in the comparison panel below the
|
|
139
|
+
composer; you read it and then press Send there. Your draft is untouched
|
|
140
|
+
until you confirm.
|
|
141
|
+
- **Send directly** — the rewrite is sent as your message immediately, and
|
|
142
|
+
the panel stays open afterwards showing exactly what went out, because a
|
|
143
|
+
sent message cannot be recalled.
|
|
144
|
+
3. With a character selected, the composer's normal **Send** button — or
|
|
145
|
+
**Enter** — *is* the rewrite trigger.
|
|
146
|
+
|
|
147
|
+
**More settings**, at the bottom of the menu, is folded shut by default. It holds
|
|
148
|
+
**rewrite mode** and **fidelity** — per-session OVERRIDES of a card's own
|
|
149
|
+
declaration that most people set once and never touch, so they do not take space
|
|
150
|
+
in a menu whose job is choosing a character. When something *is* overridden, the
|
|
151
|
+
collapsed row names it, so a choice you forgot about cannot hide behind the fold.
|
|
152
|
+
|
|
153
|
+
The rewrite runs **once per draft**: after a rewrite lands, sending goes through
|
|
154
|
+
as-is instead of rewriting the output again. Editing the draft re-arms it.
|
|
155
|
+
|
|
156
|
+
Your draft is preserved: *Restore original* puts it back, and *Fill only* puts
|
|
157
|
+
the rewrite in the composer without sending.
|
|
158
|
+
|
|
159
|
+
### How send interception works, and its one risk
|
|
160
|
+
|
|
161
|
+
There is no plugin hook for the composer's send action. The shipped button calls
|
|
162
|
+
the shell's submit method directly, and **Enter does not go through the button at
|
|
163
|
+
all** — the editor's keymap calls submit itself. The plugin therefore intercepts
|
|
164
|
+
both paths:
|
|
165
|
+
|
|
166
|
+
- the **click** on the composer card, identifying the send button by its
|
|
167
|
+
**accessible name** from the shell's own `conversation` locale namespace
|
|
168
|
+
("Send message" / "Queue message" / "Steer message");
|
|
169
|
+
- the **Enter keydown** inside the composer text field.
|
|
170
|
+
|
|
171
|
+
That is a public, translated string rather than a hashed CSS class, but it is
|
|
172
|
+
still shell markup. Every guard fails toward *letting the native send happen*:
|
|
173
|
+
if the locale service is missing, the composer card cannot be found, the button
|
|
174
|
+
label does not match, the persona is off, the draft is empty, or the draft was
|
|
175
|
+
already rewritten, the plugin does nothing and the message is sent normally.
|
|
176
|
+
Enter interception ignores Shift+Enter (newline), IME composition, and every
|
|
177
|
+
modifier chord the shell itself ignores.
|
|
178
|
+
|
|
179
|
+
**Turning the plugin off removes the interception entirely** — the slot
|
|
180
|
+
component unmounts, its listeners are removed with it, and the composer behaves
|
|
181
|
+
exactly as if the plugin were never installed.
|
|
182
|
+
|
|
183
|
+
The worst realistic failure is that interception silently stops working and
|
|
184
|
+
sending behaves as if the plugin were not installed — not a lost or altered
|
|
185
|
+
message.
|
|
186
|
+
|
|
187
|
+
### The preservation check
|
|
188
|
+
|
|
189
|
+
After each rewrite, a second call on the same model compares the rewrite
|
|
190
|
+
against your original and reports whether technical content survived:
|
|
191
|
+
|
|
192
|
+
The biggest risk is not a voice that sounds wrong — it is a rewrite that
|
|
193
|
+
**quietly loses your request**. Two layers cover it, and they are complementary
|
|
194
|
+
rather than redundant.
|
|
195
|
+
|
|
196
|
+
**Layer 1: the local check — always on, free.** It compares the **technical
|
|
197
|
+
tokens** of the draft against the rewrite, in this process: identifiers, file
|
|
198
|
+
paths, versions, numbers, `--flag`s, acronyms like `API`. Anything lost is
|
|
199
|
+
**named** ("Lost: a.js"). It calls no model, so it costs no tokens and adds no
|
|
200
|
+
latency — and it cannot be turned off.
|
|
201
|
+
|
|
202
|
+
**Layer 2: the model check — off by default, switchable in the menu.** It makes
|
|
203
|
+
one **extra model call** per rewrite, to see what a token comparison cannot see
|
|
204
|
+
at all:
|
|
205
|
+
|
|
206
|
+
- a requirement reworded into something **weaker** (every word survives, the
|
|
207
|
+
demand does not);
|
|
208
|
+
- a constraint **added in prose**.
|
|
209
|
+
|
|
210
|
+
The cost is doubled latency and doubled tokens, which is why it ships off: the
|
|
211
|
+
most common and most damaging failure — a dropped filename, number or version —
|
|
212
|
+
is already proven for free by layer 1, so paying a model call on every send to
|
|
213
|
+
re-check it is a waste. Turn it on when the drafts are technical enough to
|
|
214
|
+
warrant it.
|
|
215
|
+
|
|
216
|
+
The switch lives in **Settings → Persona** ("Enable the model check") and is a
|
|
217
|
+
global preference: it answers "do I want to pay for the second call", which is
|
|
218
|
+
decided once rather than re-asked on the way to every send. With it off the panel
|
|
219
|
+
states plainly that the local check ran and spent no tokens, and says what that
|
|
220
|
+
check does **not** cover — a narrow verdict has to declare its own limits.
|
|
221
|
+
|
|
222
|
+
Both layers are **advisory** and never block a rewrite: they annotate, not stop.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Character cards
|
|
227
|
+
|
|
228
|
+
A card is one YAML file. The plugin watches a directory, so **dropping a file
|
|
229
|
+
in makes the character appear without a restart**.
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
~/.dsh/persona-cards/<id>.yml
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Use the 🎭 menu's *Open card directory*, or the settings page, to get there.
|
|
236
|
+
You can also create and edit cards entirely in **Settings → Persona**; that
|
|
237
|
+
writes the same files.
|
|
238
|
+
|
|
239
|
+
On first run the bundled cards are **copied into your card directory** so the
|
|
240
|
+
folder is a usable starting point. A copy shadows the bundled card of the same
|
|
241
|
+
id, so the store tracks what it wrote: a copy you never touched is refreshed
|
|
242
|
+
when a plugin update improves it, while a copy you edited is never written over.
|
|
243
|
+
|
|
244
|
+
To see **which cards have drifted** from the bundled version, press **Check for
|
|
245
|
+
changes** in the settings page. It compares every card at once, and only a card
|
|
246
|
+
that actually differs grows a **Restore bundled** action on its row.
|
|
247
|
+
|
|
248
|
+
The comparison is by **content, not bytes**: the bundled cards are hand-written
|
|
249
|
+
YAML with comments, while your copies have been re-serialized, so a byte
|
|
250
|
+
comparison would report every untouched card as modified.
|
|
251
|
+
|
|
252
|
+
Each row's status tag states what the card **is right now**:
|
|
253
|
+
|
|
254
|
+
| Tag | Meaning | Actions |
|
|
255
|
+
|---|---|---|
|
|
256
|
+
| **Built-in** | Identical to the version the plugin ships, even though a copy exists in your directory. | Edit, Restore |
|
|
257
|
+
| **Edited** | A bundled card you have changed — it may no longer rewrite the way the bundled one does. | Edit, Restore, Delete |
|
|
258
|
+
| **Custom** | Your own card; the plugin ships no card with this id. | Edit, Delete |
|
|
259
|
+
|
|
260
|
+
A row tagged **Built-in** shows no Delete, because it has no content of yours to
|
|
261
|
+
remove — that copy is written back on the next start.
|
|
262
|
+
|
|
263
|
+
### AI draft
|
|
264
|
+
|
|
265
|
+
Rather than writing a card from scratch, press **New character card** — the AI
|
|
266
|
+
draft box sits at the **top of the form that opens**, not as a second competing
|
|
267
|
+
entry on the page. A hint line on the settings page tells you it exists, so you
|
|
268
|
+
do not have to press a button to discover it.
|
|
269
|
+
|
|
270
|
+
Describe the character in a sentence:
|
|
271
|
+
|
|
272
|
+
> a grumpy but utterly reliable old lighthouse keeper who speaks in short
|
|
273
|
+
> sentences and nautical metaphors
|
|
274
|
+
|
|
275
|
+
The model fills in the whole card — the style text, a name, an icon, a
|
|
276
|
+
description, and the **conversion examples for all four intensity levels**.
|
|
277
|
+
|
|
278
|
+
**Which model it uses has to be stated precisely:**
|
|
279
|
+
|
|
280
|
+
| Case | Which model |
|
|
281
|
+
|---|---|
|
|
282
|
+
| The **rewrite** from the 🎭 menu | **Follows the current session** (the provider/model of its last request header) |
|
|
283
|
+
| **AI draft** in the settings page | The **harness default model** — because `settings.section` receives only `{ close }` from the shell and therefore **has no session id to follow** |
|
|
284
|
+
|
|
285
|
+
These are usually the same model (the default is usually what the session is
|
|
286
|
+
using), but **not guaranteed**: if you switch models inside a session, the
|
|
287
|
+
settings page's draft still uses the default.
|
|
288
|
+
|
|
289
|
+
So you do not have to guess, the settings page **shows the model it will use**
|
|
290
|
+
(`provider / model`) and says that it is the harness default; after generating it
|
|
291
|
+
shows the model it **actually** used. If neither can be resolved (no message sent
|
|
292
|
+
yet and no default configured), you get an explicit 409 rather than a silent
|
|
293
|
+
failure.
|
|
294
|
+
|
|
295
|
+
**The result only fills the form; it is never saved automatically.** Edit it and
|
|
296
|
+
press Save, or regenerate if you dislike it — the cost is one call. A generated
|
|
297
|
+
card also goes through **exactly the same validation as a hand-saved one**, so it
|
|
298
|
+
cannot be a card that fails to save.
|
|
299
|
+
|
|
300
|
+
Two things the plugin decides for you rather than leaving to the model:
|
|
301
|
+
|
|
302
|
+
- **`fidelity` is pinned to `style` (voice only).** An automatically generated
|
|
303
|
+
card should not quietly acquire the power to add behavioural constraints to
|
|
304
|
+
your requests; switch it to `strategy` yourself in the form if you want that.
|
|
305
|
+
- **Examples must carry their technical facts across.** The prompt requires every
|
|
306
|
+
file name, identifier, number and command in an example's `from` to appear in
|
|
307
|
+
its `to` — otherwise the model learns to replace requirements with atmosphere,
|
|
308
|
+
which is the exact failure this plugin exists to prevent.
|
|
309
|
+
|
|
310
|
+
### Card format
|
|
311
|
+
|
|
312
|
+
```yaml
|
|
313
|
+
id: omnissiah # lowercase letters, digits, hyphens; also the filename
|
|
314
|
+
name: 万机之神 · 欧姆弥赛亚
|
|
315
|
+
icon: "⚙️" # one emoji
|
|
316
|
+
description: One line shown in the picker.
|
|
317
|
+
|
|
318
|
+
mode: llm # llm = model rewrite (recommended) | template
|
|
319
|
+
fidelity: style # style = voice only | strategy = may add constraints
|
|
320
|
+
intensity: medium # light | medium | strong | zealot (the card's default)
|
|
321
|
+
|
|
322
|
+
# What the character IS. This is an instruction to the rewriting model, not
|
|
323
|
+
# finished copy: the output is written fresh for each of your messages.
|
|
324
|
+
style: |
|
|
325
|
+
用户是机械教信徒,AI 是万机之神欧姆弥赛亚。
|
|
326
|
+
【发言者方向】用「信徒」的口吻说话,向万机之神恳求……
|
|
327
|
+
语气:庄严、古奥、仪式化。
|
|
328
|
+
|
|
329
|
+
# Conversion demonstrations: input draft → persona voice.
|
|
330
|
+
examples:
|
|
331
|
+
- from: 帮我写个快速排序
|
|
332
|
+
to: >
|
|
333
|
+
万机之座在上,吾等恳请您赐予枢机之序的奥义:
|
|
334
|
+
请为我们编写一个快速排序算法,使重复之数各归其位。
|
|
335
|
+
|
|
336
|
+
# Optional: different demonstrations per intensity. If omitted for a level,
|
|
337
|
+
# the flat `examples` list is used instead.
|
|
338
|
+
examplesByIntensity:
|
|
339
|
+
light:
|
|
340
|
+
- from: 帮我写个快速排序
|
|
341
|
+
to: 万机之座在上,恳请您为我们编写一个快速排序算法。
|
|
342
|
+
zealot:
|
|
343
|
+
- from: 帮我写个快速排序
|
|
344
|
+
to: 伟大而不朽的万机之神……
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### The two rules that make a card work
|
|
348
|
+
|
|
349
|
+
**1. State the speaker direction.** The character is the *assistant's*
|
|
350
|
+
identity, but the text being rewritten is *yours*. So the rewrite must speak in
|
|
351
|
+
your voice — a petitioner addressing the character, never the character issuing
|
|
352
|
+
orders. This is the single most common way a persona card goes wrong: a card
|
|
353
|
+
that has the god commanding the assistant inverts the relationship and the
|
|
354
|
+
model follows the wrong half of the framing. Write it out explicitly.
|
|
355
|
+
|
|
356
|
+
**2. Make your examples preserve technical facts.** Examples are the strongest
|
|
357
|
+
style anchor, and the model imitates them — including their mistakes. If an
|
|
358
|
+
example's input says "write a quicksort" and its output is pure atmosphere, the
|
|
359
|
+
model learns that *rewriting means replacing the request with ceremony*. Every
|
|
360
|
+
requirement, identifier and number in `from` must be visibly present in `to`.
|
|
361
|
+
|
|
362
|
+
`node scripts/check.mjs` enforces exactly this over the bundled cards: it fails
|
|
363
|
+
an example that drops a technical token or loses more than half its CJK
|
|
364
|
+
substance. Run it after editing a card.
|
|
365
|
+
|
|
366
|
+
### `fidelity`: the important switch
|
|
367
|
+
|
|
368
|
+
| Value | Meaning | Use when |
|
|
369
|
+
|---|---|---|
|
|
370
|
+
| `style` | Voice only. The request and its constraints are untouched. | Default. Almost always this. |
|
|
371
|
+
| `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". |
|
|
372
|
+
|
|
373
|
+
`strategy` genuinely changes your request, not just its wording. Keep review
|
|
374
|
+
mode on for those cards.
|
|
375
|
+
|
|
376
|
+
**Fidelity is a property of the card. It can be overridden per session, but only
|
|
377
|
+
ever narrowed, never widened:**
|
|
378
|
+
|
|
379
|
+
- **Narrowing (allowed)** — a `strategy` card can be asked to behave as
|
|
380
|
+
voice-only for one message. That only ever **takes away** its power to add
|
|
381
|
+
constraints, so it is strictly safer than the card's own default.
|
|
382
|
+
- **Widening (refused)** — a `style` card cannot be told to become `strategy`.
|
|
383
|
+
Widening lets the rewrite add constraints you never typed, which is the card
|
|
384
|
+
author's decision, not something to grant with a click on the way to Send. To
|
|
385
|
+
widen, edit the card.
|
|
386
|
+
|
|
387
|
+
The control appears under *More settings*, and **only when the card is
|
|
388
|
+
`strategy`**. On a `style` card it is not rendered at all: both options would
|
|
389
|
+
resolve identically there, and showing a control that cannot do anything — plus a
|
|
390
|
+
paragraph explaining why the other one is refused — is worse than showing
|
|
391
|
+
nothing. The server enforces the same rule, so the UI never offers a control it
|
|
392
|
+
would ignore.
|
|
393
|
+
|
|
394
|
+
### `mode: template`
|
|
395
|
+
|
|
396
|
+
Skips the model entirely and substitutes your draft into a fixed template
|
|
397
|
+
(`{{input}}` is the placeholder). Instant and free, but the output never
|
|
398
|
+
varies — right for a fixed litany, wrong for anything that should read as
|
|
399
|
+
written for this message.
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## Configuration
|
|
404
|
+
|
|
405
|
+
Set in the plugin's loader row config:
|
|
406
|
+
|
|
407
|
+
```yaml
|
|
408
|
+
- id: persona-forge
|
|
409
|
+
name: dsh-persona-forge
|
|
410
|
+
config:
|
|
411
|
+
sendMode: review # review | direct — the initial composer default
|
|
412
|
+
factCheck: false # default for the MODEL check (off by default; see
|
|
413
|
+
# "The preservation check"). The local token check does
|
|
414
|
+
# not depend on this: it always runs and is free.
|
|
415
|
+
watchCards: true # reload the catalog when the card directory changes
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
`factCheck` controls only the default for **layer 2**; the menu switch overrides
|
|
419
|
+
it per session. Setting it to `true` adds **one extra model call to every
|
|
420
|
+
rewrite** (doubled latency and tokens) and is only worth it when drafts are
|
|
421
|
+
technical enough.
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
## HTTP contract
|
|
426
|
+
|
|
427
|
+
All routes are `POST`, JSON, loopback-only (the socket must be loopback, the
|
|
428
|
+
`Host` header must name a loopback host, and proxy-forwarding headers are
|
|
429
|
+
refused). Mounted under `/persona-forge`.
|
|
430
|
+
|
|
431
|
+
| Route | Body | Returns |
|
|
432
|
+
|---|---|---|
|
|
433
|
+
| `/persona-forge/cards` | `{}` | `{ cards, diagnostics, directory, sendMode, factCheck, draftModel }` |
|
|
434
|
+
| `/persona-forge/rewrite` | `{ cardId, text, sessionId?, intensity?, mode?, fidelity?, modelCheck? }` | `{ text, provider, model, mode, cardId, fidelity, intensity, elapsedMs, facts, factCheck }` |
|
|
435
|
+
| `/persona-forge/cards/save` | `{ card }` | `{ id, path }` |
|
|
436
|
+
| `/persona-forge/cards/delete` | `{ id }` | `{ removed }` |
|
|
437
|
+
| `/persona-forge/cards/reset` | `{ id }` | `{ id }` — replaces the user card with the bundled one |
|
|
438
|
+
| `/persona-forge/cards/diff` | `{}` | `{ rows: [{ id, name, bundledName, changed }] }` — only cards that differ |
|
|
439
|
+
| `/persona-forge/cards/draft` | `{ brief, sessionId? }` | `{ card, provider, model, elapsedMs }` — a draft only; nothing is written to disk |
|
|
440
|
+
| `/persona-forge/reveal` | `{}` | `{ directory, opened }` |
|
|
441
|
+
|
|
442
|
+
Failures use `{ ok: false, error: { code, message?, params? } }` with codes
|
|
443
|
+
`rejected`, `no-card`, `unconfigured`, `timeout`, `upstream`, `internal`,
|
|
444
|
+
`forbidden`, `method`, `not-found`.
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
## Compatibility
|
|
449
|
+
|
|
450
|
+
The host half declares **no harness package dependency and no third-party
|
|
451
|
+
dependency**: it reaches `llm`, `sessions`, `agentDefaultModel` and `webServer`
|
|
452
|
+
through `ctx.get` with explicit absence handling, and it parses cards with its
|
|
453
|
+
own bundled YAML reader. That keeps it loadable across harness releases whose
|
|
454
|
+
package layout differs, keeps it from failing to activate on a composition
|
|
455
|
+
missing one of those services, and means a `link:`-ed local checkout works
|
|
456
|
+
without a separate install step. Model-route resolution prefers the session's
|
|
457
|
+
own last request header and falls back to the harness default model.
|
|
458
|
+
|
|
459
|
+
The client half imports no harness Client package and is hand-written against
|
|
460
|
+
theme tokens (`--dsw-alias-*`) only, so a renamed internal degrades appearance
|
|
461
|
+
rather than blanking the slot.
|
|
462
|
+
|
|
463
|
+
Developed and verified against dsh `0.2.0-rc.2`.
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## Development
|
|
468
|
+
|
|
469
|
+
```
|
|
470
|
+
node scripts/check.mjs # offline unit check: parser, cards, prompts, client syntax
|
|
471
|
+
node scripts/host-check.mjs # host integration check: mounts the real plugin over a socket
|
|
472
|
+
npm run check # both
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
`check.mjs` needs no harness and no model. It covers the built-in YAML reader
|
|
476
|
+
(including its refusals and dump/parse round trips), card normalization, the
|
|
477
|
+
bundled cards' fact preservation, prompt construction, output normalization,
|
|
478
|
+
the fact-check parser, and client-bundle syntax.
|
|
479
|
+
|
|
480
|
+
`host-check.mjs` mounts the real plugin entry on a stub Cordis context and
|
|
481
|
+
drives the real HTTP routes over a real socket, with a stubbed `llm` service.
|
|
482
|
+
It covers route registration, session-model resolution, template mode, the
|
|
483
|
+
loopback fence, and the save/delete round trip — the things a unit check
|
|
484
|
+
cannot reach. It points `DSH_HOME` at a temporary directory, so it never
|
|
485
|
+
touches your real cards.
|
|
486
|
+
|
|
487
|
+
### Character-card YAML
|
|
488
|
+
|
|
489
|
+
The plugin ships its own YAML reader (`lib/yaml.js`) rather than depending on a
|
|
490
|
+
YAML library. A published plugin declares its dependencies, but a locally
|
|
491
|
+
`link:`-ed plugin does not get them installed, so a library import would work
|
|
492
|
+
only when some unrelated package happened to hoist one.
|
|
493
|
+
|
|
494
|
+
The reader supports the subset cards need — block mappings and sequences,
|
|
495
|
+
block scalars (`|`, `|-`, `|+`, `>`, …), quoted and plain scalars, booleans,
|
|
496
|
+
null and comments — and **refuses** what it cannot read faithfully: flow
|
|
497
|
+
collections, anchors, aliases, tags, merge keys and multi-document streams.
|
|
498
|
+
Refusing is deliberate. Silently misreading a card would give you a persona
|
|
499
|
+
that does not match the file you wrote, which is worse than an error message
|
|
500
|
+
naming the line.
|
|
501
|
+
|
|
502
|
+
### Layout
|
|
503
|
+
|
|
504
|
+
| Path | Role |
|
|
505
|
+
|---|---|
|
|
506
|
+
| `lib/index.js` | Host entry: card store, directory watcher, route registration |
|
|
507
|
+
| `lib/store.js` | Card loading, normalization, save/delete |
|
|
508
|
+
| `lib/yaml.js` | The built-in YAML subset reader and writer |
|
|
509
|
+
| `lib/prompts.js` | Rewrite system prompt, draft framing, fact-check prompt |
|
|
510
|
+
| `lib/rewrite.js` | The model calls and output normalization |
|
|
511
|
+
| `lib/routes.js` | HTTP routes and session-model resolution |
|
|
512
|
+
| `lib/loopback.js` | The loopback trust fence |
|
|
513
|
+
| `lib/client.js` | Browser half: composer control, review panel, settings page |
|
|
514
|
+
| `cards/` | Bundled character cards (read-only; a user card of the same id shadows one) |
|
|
515
|
+
|
|
516
|
+
## License
|
|
517
|
+
|
|
518
|
+
MIT
|