dsh-agent-persona 0.1.0 → 0.1.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 +130 -146
- package/README.md +33 -14
- package/README_EN.md +45 -15
- package/lib/client.js +621 -216
- package/lib/index.d.ts +18 -0
- package/lib/index.js +155 -17
- package/package.json +4 -4
- package/scripts/install.ps1 +54 -6
- package/scripts/install.sh +57 -10
package/CHANGELOG.md
CHANGED
|
@@ -4,44 +4,141 @@ All notable changes to this project are documented here.
|
|
|
4
4
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
-
## [
|
|
7
|
+
## [0.1.1] - 2026-09-20
|
|
8
8
|
|
|
9
9
|
### Added
|
|
10
10
|
|
|
11
|
+
- **A readable store is protected before it is replaced.** A store file that exists but cannot be read used to
|
|
12
|
+
look exactly like an empty one, and the next save replaced it. The page now says what happened and why, and
|
|
13
|
+
the first write copies the original to `personas.json.bak-<timestamp>` before touching it. If the copy fails
|
|
14
|
+
the write fails with it, so content nothing preserved is never destroyed.
|
|
15
|
+
- **The persona store is no longer writable by tools.** A monotonic `ctx.tools.guard` refuses any call whose
|
|
16
|
+
arguments point at the store and any shell command that mentions it together with a write feature
|
|
17
|
+
(`>`, `rm`, `mv`, `cp`, `chmod`, `sed -i`, `tee`, `truncate`, …), while reads (`cat`, `md5`, …) stay allowed.
|
|
18
|
+
A session running under a persona can therefore no longer rewrite or delete that persona. It is a
|
|
19
|
+
**preference** (`禁止模型改写人设文件`), on by default, and the guard reads it live so a toggle applies to the
|
|
20
|
+
next tool call. It is a behavioural limit, not a security boundary, and the switch says so on hover.
|
|
21
|
+
- **导入 / 导出.** Export writes a JSON file (and copies it to the clipboard); import takes a chosen file or a
|
|
22
|
+
pasted document, **previews as you type** every persona with its size and reach, and creates them through the
|
|
23
|
+
same `savePersona` call the page uses, so a document cannot bypass the host's validation. Imported personas are
|
|
24
|
+
always created **disabled** and appended at the end, and the default slot is never taken over. The dialog
|
|
25
|
+
colours the JSON in place, draws its own thin scrollbar, and offers the replace-everything restore behind a
|
|
26
|
+
switch that names the count it would delete.
|
|
27
|
+
- **A release workflow** (`.github/workflows/release.yml`): publishing a GitHub Release whose tag matches
|
|
28
|
+
`package.json` runs the same build/typecheck/test gates as CI and publishes to npm through Trusted Publishing
|
|
29
|
+
(OIDC) — no `NPM_TOKEN`, provenance attached, prereleases going to the `next` dist-tag.
|
|
30
|
+
- Source links and the running version beside the settings title: GitHub and npm as two small pills carrying the
|
|
31
|
+
real brand marks (inlined from simple-icons, `currentColor`, no hardcoded colours), plus the version of the
|
|
32
|
+
copy actually running. The version comes from the host reading its own `package.json`, so a checkout and an npm
|
|
33
|
+
install report different things — and the tooltip says which one this is. The same two fields go into the
|
|
34
|
+
load heartbeat.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- **The header spacing follows the host's own rhythm.** The title, description and counts now sit on a uniform
|
|
39
|
+
12px gap, and the extra 10px margin on the preference row is gone: the counts, the switches and the cards are
|
|
40
|
+
14px apart, the distance the settings shell itself uses between blocks.
|
|
41
|
+
- The reported test counts are counted rather than written down: both suites wrap `assert` and report what ran,
|
|
42
|
+
so the number can no longer fall behind the file (200 host assertions, 165 client assertions).
|
|
43
|
+
- **Opening and closing a card is one CSS transition**, driven by the class the card already carries: a grid row
|
|
44
|
+
interpolating between `0fr` and `1fr`, over one easing and one duration in both directions. Nothing is timed,
|
|
45
|
+
nothing is unmounted, and no transform is involved, so a fixed-position tooltip can never be captured by it.
|
|
46
|
+
The scope line under the header collapses in the opposite direction over the same span — it used to be mounted
|
|
47
|
+
and unmounted with the state, which made the whole card jump by its height the moment a card was opened.
|
|
48
|
+
- The card's chevron turns exactly **90°**: it keeps one glyph, rotated by CSS. Swapping in a down-chevron as
|
|
49
|
+
well turned it twice, so an open card pointed the wrong way.
|
|
50
|
+
- **Each preference explains itself on hover.** The switches used to carry their own label as a native tooltip,
|
|
51
|
+
which explained nothing: `保护人设文件` became `禁止模型改写人设文件`, and every switch now has a real tooltip
|
|
52
|
+
(also folded into its accessible name) saying what it does and what it does not.
|
|
53
|
+
- **Nothing is added to Settings → General.** The switches live on the Agent 人设 page alone, which none of them
|
|
54
|
+
can hide, so that page is the way back from any of them.
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
|
|
58
|
+
- A note about a preserved store no longer disappears the moment it becomes true: a reload only clears the
|
|
59
|
+
record for a *fresh* reset, not for the stamp change caused by our own write.
|
|
60
|
+
- **Tooltips opened inside the view or the sidebar card landed in the wrong place.** The entry animations filled
|
|
61
|
+
forwards, and a transform keyframe that ends at `transform: none` still leaves an identity matrix — which
|
|
62
|
+
counts as a transform, and therefore made the panel the containing block for the UI kit's fixed-position
|
|
63
|
+
bubble. Every animation now fills backwards, so the final state is the element's own style, and reduced-motion
|
|
64
|
+
covers the new ones too.
|
|
65
|
+
- **`scripts/install.sh` and `scripts/install.ps1` were rewritten**: they still carried the pre-rename package
|
|
66
|
+
name, the old heartbeat path and the old page title. They now install `dsh-agent-persona`, print the version of
|
|
67
|
+
the checkout, state the real store and heartbeat paths, offer upgrade and removal, and refuse a path target
|
|
68
|
+
that does not exist — the CLI writes a `link:` dependency for whatever it is given, so a typo silently became a
|
|
69
|
+
broken profile entry.
|
|
70
|
+
|
|
71
|
+
## [0.1.0] - 2026-09-17
|
|
72
|
+
|
|
73
|
+
### Added
|
|
74
|
+
|
|
75
|
+
- **Many personas, each scoped independently.** A persona is a first-class record
|
|
76
|
+
(`id` / `name` / `enabled` / `text` / `targets[]`), and every target is
|
|
77
|
+
`{ kind: 'workspace' | 'sessionId', match: 'exact' | 'prefix' | 'regex' | 'contains', value }`.
|
|
78
|
+
- **An explicit default persona** (`默认人设`) with a `fallback` flag: it takes every session no reach rule
|
|
79
|
+
claimed, and only one persona can hold it — marking a new one releases the previous.
|
|
80
|
+
- **Dropdown scope pickers.** The settings page fills its pickers from the host's
|
|
81
|
+
workspaces and from DSH's own sessions (newest first, with title, working
|
|
82
|
+
directory and a relative timestamp), so nobody has to type a path or a session
|
|
83
|
+
id. The two kinds are mutually exclusive per row — switching a row's type clears
|
|
84
|
+
its value — and "自己输入…" keeps the advanced match modes reachable.
|
|
85
|
+
- **System-prompt injection** as section `agent-persona` at order 1
|
|
86
|
+
(`DEPLOYMENT_PERSONA_PREFIX + 1`), i.e. above the workspace `AGENTS.md`, with
|
|
87
|
+
`append` (default) and `replace` injection modes.
|
|
11
88
|
- **A 人设 tab on the conversation page**: which persona the session matched, the system prompt it actually sent
|
|
12
89
|
(rendered and source views), and the full management UI in place.
|
|
13
|
-
- **An explicit default persona** (`默认人设`): it takes every session no reach rule claimed, and only one persona
|
|
14
|
-
can hold the flag — marking a new one releases the previous.
|
|
15
|
-
- **Interface preferences** — `编辑后自动保存`, `在对话页显示「人设」标签`, `注册到 dsh-better-sidebar` — stored in the
|
|
16
|
-
persona file's `prefs` field, so they share the data's scope; the browser keeps only a load-time mirror.
|
|
17
90
|
- **A sidebar card** for `dsh-better-sidebar`, registered through that plugin's own service. The switch that
|
|
18
91
|
controls it is **not shown at all** when that plugin is absent.
|
|
92
|
+
- **Interface preferences** — `编辑后自动保存`, `在对话页显示「人设」标签`, `注册到 dsh-better-sidebar` — stored in the
|
|
93
|
+
persona file's `prefs` field, so they share the data's scope; the browser keeps only a load-time mirror.
|
|
94
|
+
- **Session history**: pick a session, hit the button next to the picker, and read that conversation. Reads are
|
|
95
|
+
windowed (`sessionHistory({id, offset, events})`) so the log is never read up front: the page shows what it
|
|
96
|
+
got, reports 已显示 N 条, and only reads the next window when 继续加载 is pressed. Messages are capped at 4000
|
|
97
|
+
characters, and 展开全文 replaces a single bubble by reading that one event again (`offset: at, events: 1`).
|
|
98
|
+
- **AI rework** (polish / expand / compress / draft) that uses the model DSH itself
|
|
99
|
+
is set to use, overridable per deployment with `tuneProvider` / `tuneModel`. It
|
|
100
|
+
only ever produces a proposal.
|
|
101
|
+
- **Search** appears once a profile holds four or more personas and filters on name, workspace title/path and
|
|
102
|
+
session title/id, with an explicit 「没有匹配「…」的人设」 state.
|
|
103
|
+
- **Drag to reorder**: a grip on each card header drags it to an absolute position (new `reorderPersona` remote
|
|
104
|
+
method, `reorderPersonas` is the exported pure function); the ⋯ menu's 上移/下移 stay as the keyboard path.
|
|
19
105
|
- **A Schemastery config**: `storePath`, `tuneProvider`, `tuneModel`, validated when the row loads.
|
|
20
106
|
- **TypeScript sources with a build**: `src/**` compiles to `lib/`, and a generator derives the remote artifacts
|
|
21
107
|
(`src/remote.ts`, `src/typert.ts`) from the single endpoint list in `src/endpoints.ts`; `npm test` fails if the
|
|
22
108
|
generated files drift.
|
|
109
|
+
- **One directory for the data**: `$DSH_HOME/dsh-agent-persona/` holds
|
|
110
|
+
`personas.json` and a `state.json` heartbeat. The data is re-read on every prompt
|
|
111
|
+
assembly (mtimeNs/size stamp), so an edit applies to the next message.
|
|
112
|
+
- **Zero runtime dependencies** for the host: the client half is bundled by esbuild, the host half is plain ESM,
|
|
113
|
+
and `npm test` runs both offline suites.
|
|
23
114
|
|
|
24
115
|
### Changed
|
|
25
116
|
|
|
26
|
-
- **
|
|
27
|
-
|
|
117
|
+
- **Resolution is specificity-first.** A session-id rule outranks a workspace rule and an exact value outranks a
|
|
118
|
+
pattern (`targetSpecificity`); list order only breaks ties between equally specific rules. A persona pointed at
|
|
119
|
+
one exact session therefore beats a persona pointed at the whole directory, whatever the order.
|
|
120
|
+
- **A persona with no reach applies nowhere.** A place-less persona is a draft, and where nothing matched the
|
|
121
|
+
harness prompt stands; `默认人设` is the explicit way to ask for a fallback.
|
|
28
122
|
- **Replace mode also drops the harness identity line**, not only the deployment's persona line.
|
|
123
|
+
- **Exactly one persona per workspace or session.** A save that claims a place takes it away from the persona
|
|
124
|
+
that held it (exact values only, reported back to the page), and the pickers label places that are already
|
|
125
|
+
taken.
|
|
126
|
+
- The collapsed card keeps only what reads at a glance — chevron, state dot, name, state, scope chips — and
|
|
127
|
+
reordering / duplicating / deleting moved into the card's `⋯` menu.
|
|
128
|
+
- The status line is sticky at the bottom of the page so save/error feedback stays visible while the list
|
|
129
|
+
scrolls; icon buttons grew from 24px to 28px hit targets.
|
|
130
|
+
- The session picker shows how often each session was talked to (`sessionStats.turns`), beside the short id and
|
|
131
|
+
age, and sessions are labelled with their DSH title or their first prompt.
|
|
132
|
+
- Renamed the package from `dsh-workspace-persona` to **`dsh-agent-persona`**. Everything it writes now lives in
|
|
133
|
+
`$DSH_HOME/dsh-agent-persona/`.
|
|
29
134
|
- Display preferences take effect immediately rather than on the next page load: the conversation view and the
|
|
30
135
|
sidebar card are registered and unregistered at runtime.
|
|
31
136
|
|
|
32
137
|
### Fixed
|
|
33
138
|
|
|
34
|
-
-
|
|
35
|
-
`
|
|
36
|
-
|
|
37
|
-
omits the flag keeps the stored value instead of clearing it.
|
|
38
|
-
- The session view's own draft and dirty check cover the default flag, and its switch no longer calls a method
|
|
39
|
-
that only the settings component has. Together those made the switch look inert and let a save turn it off.
|
|
40
|
-
|
|
41
|
-
## [0.1.0] - 2026-09-17
|
|
42
|
-
|
|
43
|
-
### Fixed
|
|
44
|
-
|
|
139
|
+
- **跳到最新 did nothing**: the runtime forwarded a hand-written subset of the history options and dropped
|
|
140
|
+
`tail` / `keep`, so the call degraded to a normal head read while the dialog labelled it as the tail. The
|
|
141
|
+
forwarding is now an exported, asserted `historyOptions`.
|
|
45
142
|
- 跳到最新 kept a fixed four-message window, so a turn where the agent answered several times pushed the user's
|
|
46
143
|
question out of it. The tail now returns everything from the **last user input** onwards (bounded at 60
|
|
47
144
|
messages while walking).
|
|
@@ -49,137 +146,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
49
146
|
bubbles show raw Markdown. Messages are now split into bounded Markdown-safe chunks (blank-line boundaries,
|
|
50
147
|
never inside a code fence) and each chunk renders on its own: one awkward chunk degrades with a visible note,
|
|
51
148
|
the rest of the message still renders.
|
|
52
|
-
- `npm test` now also runs a small client-side suite (`test/client-helpers.test.mjs`) that lifts the client's pure
|
|
53
|
-
helpers out of the source and asserts the splitter is lossless and fence-safe.
|
|
54
|
-
|
|
55
|
-
- 跳到最新 did nothing: the runtime forwarded a hand-written subset of the history options and dropped ,
|
|
56
|
-
so the call degraded to a normal head read while the dialog labelled it as the tail. The forwarding is now an
|
|
57
|
-
exported, asserted .
|
|
58
|
-
### Added
|
|
59
|
-
|
|
60
|
-
- **展开全文 per bubble.** Every message now carries (its event index in the log), so a message that was
|
|
61
|
-
clipped at 1000 characters offers 展开全文: one
|
|
62
|
-
read replaces that single bubble with the full text (and 收起全文 puts the short text back without re-reading).
|
|
63
|
-
Nothing else in the dialog is re-fetched.
|
|
64
|
-
### Changed
|
|
65
|
-
|
|
66
|
-
- The per-message text cap is 1000 characters (clipping stays markdown-safe), and 跳到最新 no longer appends the
|
|
67
|
-
rest of the conversation: it walks to the end and replaces the view with the last exchange only, so one click
|
|
68
|
-
answers "what were we just talking about". 从头看 returns to the sequential view, whose footer states that it
|
|
69
|
-
reads from the beginning.
|
|
70
|
-
### Fixed
|
|
71
|
-
|
|
72
|
-
- **Empty sessions are no longer offered.** A session created with a preset but never typed into
|
|
73
|
-
(`sessionListMetadata.blank`, the same flag the sidebar hides on — found as a 5-event, 0.5 KB,
|
|
74
|
-
`agentPreset: 'cordis'` session) holds no conversation to give a persona, so the picker drops it and counts it
|
|
75
|
-
in the hidden tally.
|
|
76
|
-
|
|
77
|
-
- **Sub-agent and archived sessions no longer appear in the picker.** A sub-agent session (spawned by the
|
|
78
|
-
model: `origin: 'subagent'` or `delegationDepth > 0`) and an archived one cannot be opened or typed into
|
|
79
|
-
from the sidebar, so listing them invited personas that could never apply. Archived ids come from the
|
|
80
|
-
workspace registry, falling back to the workspace store file. The page reports how many were hidden, so a
|
|
81
|
-
missing session is never a mystery.
|
|
82
|
-
|
|
83
149
|
- **Sessions could not be read at all**: `sessionPersistence.open(id, access)` takes the access mode as a
|
|
84
150
|
second, required argument; the reader called `open(id)` and every candidate failed, so the conversation
|
|
85
151
|
dialog always reported "no reader". It now opens with `'read'` (never taking write ownership) and retries
|
|
86
152
|
id-only signatures for version tolerance.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
- **
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
- The
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
- Renamed the package from `dsh-workspace-persona` to **`dsh-agent-persona`** (settings page: 「Agent人设」).
|
|
102
|
-
Everything it writes now lives in `$DSH_HOME/dsh-agent-persona/`.
|
|
103
|
-
- The collapsed card keeps only what reads at a glance — chevron, state dot, name, state, scope chips — and
|
|
104
|
-
reordering / duplicating / deleting moved into the card's `⋯` menu.
|
|
105
|
-
|
|
106
|
-
### Added
|
|
107
|
-
|
|
108
|
-
- **Exactly one persona per workspace or session.** A save that claims a place takes it away from the persona
|
|
109
|
-
that held it (exact values only, reported back to the page), and the pickers label places that are already
|
|
110
|
-
taken.
|
|
111
|
-
- **Injection modes** `append` (default) and `replace`, implemented by listening to the
|
|
112
|
-
`system-prompt/assemble` waterfall and dropping the deployment's own `deployment:persona-prefix` line for
|
|
113
|
-
`replace`. A preset or subagent persona that shadowed the section is never overwritten.
|
|
114
|
-
- **Session history**: pick a session, hit the button next to the picker, and read that conversation. Reads are
|
|
115
|
-
windowed (`sessionHistory({id, offset, events})`) so the log is never read up front: the page shows what it
|
|
116
|
-
got, reports 已显示 N 条, and only reads the next window when 继续加载 is pressed. Messages are capped at 4000
|
|
117
|
-
characters. Picker rows carry a short session id and the session's age on the right.
|
|
118
|
-
- **Search** appears once a profile holds four or more personas and filters on name, workspace title/path and
|
|
119
|
-
session title/id, with an explicit 「没有匹配「…」的人设」 state.
|
|
120
|
-
- **Drag to reorder**: a grip on each card header drags it to an absolute position (new `reorderPersona` remote
|
|
121
|
-
method, `reorderPersonas` is the exported pure function); the ⋯ menu's 上移/下移 stay as the keyboard path,
|
|
122
|
-
and dragging is disabled while a search filter is active so a drop target is never ambiguous.
|
|
123
|
-
- **Conflicts are visible**: a persona whose exact row would be shadowed by an *earlier* persona's prefix /
|
|
124
|
-
regex / contains row gets a 「可能被覆盖」 tag (hover explains which rule wins), plus a summary line above the
|
|
125
|
-
list. Exact duplicates never reach this state — saving takes the claim over instead.
|
|
126
|
-
- **The save shortcut is visible**: a ⌘S / Ctrl+S key chip sits next to the save button and the button carries
|
|
127
|
-
the same hint in its tooltip.
|
|
128
|
-
- The persona counts are two tags — 「N 条人设」 and a success-toned 「N 条在用」 — instead of a sentence in the
|
|
129
|
-
meta line, and the store path moved to the right of them.
|
|
130
|
-
- **Unsaved changes are visible**: the open card carries a 未保存 tag while its draft differs from what is
|
|
131
|
-
stored, and switching cards or collapsing reports 「改动没有保存」 in the status line instead of dropping it
|
|
132
|
-
silently.
|
|
133
|
-
- The status line is sticky at the bottom of the page so save/error feedback stays visible while the list
|
|
134
|
-
scrolls; icon buttons grew from 24px to 28px hit targets.
|
|
135
|
-
- The section header now matches the shipped settings pages exactly: an `<h2>` title at 18px/600, a 13px
|
|
136
|
-
`p` intro in the muted label colour, no leading icon (the count moved into the meta line). Measured side by
|
|
137
|
-
side against the official 「Agent 预设」 page in the same DOM: identical font size, weight and colour.
|
|
138
|
-
- The page is titled with the Agent-preset glyph (the same one the settings shell uses for 「Agent 预设」); the
|
|
139
|
-
settings **nav** icon itself is the shell's and cannot be set by a plugin — `settings.section` accepts only
|
|
140
|
-
`id` / `order` / `label`, and the shell falls back to one generic glyph for every id it does not ship.
|
|
141
|
-
- Creating a persona moved out of the page's top-right corner into a dashed **新建人设** row at the end of the
|
|
142
|
-
list (the conventional place for an "add" affordance), keyboard reachable via Enter/Space; the header is now
|
|
143
|
-
just the title and the count.
|
|
144
|
-
- A collapsed card shows its scope on a **second line**, workspaces and sessions apart, with human names
|
|
145
|
-
(workspace titles, session titles) instead of raw paths; the picker marks a claimed target as 当前人设 when it
|
|
146
|
-
belongs to the persona being edited, instead of appearing to be someone else's.
|
|
147
|
-
- The conversation dialog is a two-sided chat (your input right, the agent left, both in bubbles) and hides
|
|
148
|
-
plugin-injected user-side content — `AGENTS.md` (13k chars in a real session), runtime snapshots, the skill
|
|
153
|
+
- **Empty sessions are no longer offered.** A session created with a preset but never typed into
|
|
154
|
+
(`sessionListMetadata.blank`, the same flag the sidebar hides on) holds no conversation to give a persona, so
|
|
155
|
+
the picker drops it and counts it in the hidden tally.
|
|
156
|
+
- **Sub-agent and archived sessions no longer appear in the picker.** A sub-agent session (spawned by the
|
|
157
|
+
model: `origin: 'subagent'` or `delegationDepth > 0`) and an archived one cannot be opened or typed into from
|
|
158
|
+
the sidebar, so listing them invited personas that could never apply. The page reports how many were hidden.
|
|
159
|
+
- The remote manifest is generated in the shape the mount consumes: an `id` per descriptor, a strict input codec,
|
|
160
|
+
`parameters: []` for the reads that take none, and the service name `agentPersona`.
|
|
161
|
+
- The default flag survives a store round trip again — the reader no longer drops it — and a save whose payload
|
|
162
|
+
omits the flag keeps the stored value instead of clearing it.
|
|
163
|
+
- The session view's own draft and dirty check cover the default flag, and its switch no longer calls a method
|
|
164
|
+
that only the settings component has. Together those made the switch look inert and let a save turn it off.
|
|
165
|
+
- The conversation dialog hides plugin-injected user-side content — `AGENTS.md`, runtime snapshots, the skill
|
|
149
166
|
catalog, goal rounds — reporting only 「已隐藏 N 条插件注入内容」.
|
|
150
|
-
- The scope pickers can go back from 「自己输入…」 to the list, and the match mode only appears in that
|
|
151
|
-
manual mode.
|
|
152
|
-
- Session entries in the picker are labelled with their DSH title, and with the session's first prompt (or
|
|
153
|
-
its latest prompt) when no title exists yet — all three come out of a **single read** of DSH's projection
|
|
154
|
-
cache. The dropdown never touches a session log: those are 2.6x-5.8x larger and zstd-compressed
|
|
155
|
-
append-only files that cannot be seeked.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
### Added
|
|
159
|
-
|
|
160
|
-
- **Many personas, each scoped independently.** A persona is a first-class record
|
|
161
|
-
(`id` / `name` / `enabled` / `text` / `targets[]`), and every target is
|
|
162
|
-
`{ kind: 'workspace' | 'sessionId', match: 'exact' | 'prefix' | 'regex' | 'contains', value }`.
|
|
163
|
-
- **Dropdown scope pickers.** The settings page fills its pickers from the host's
|
|
164
|
-
workspaces and from DSH's own sessions (newest first, with title, working
|
|
165
|
-
directory and a relative timestamp), so nobody has to type a path or a session
|
|
166
|
-
id. The two kinds are mutually exclusive per row — switching a row's type clears
|
|
167
|
-
its value — and "自己输入…" keeps the advanced match modes reachable.
|
|
168
|
-
- **Resolution rules**: list order is priority (first match wins), rows inside one
|
|
169
|
-
persona are OR-ed, a persona with no rows is the default persona, a disabled
|
|
170
|
-
persona is skipped, and a matching persona with an empty text means "no persona
|
|
171
|
-
here".
|
|
172
|
-
- **System-prompt injection** as section `agent-persona` at order 1
|
|
173
|
-
(`DEPLOYMENT_PERSONA_PREFIX + 1`), i.e. above the workspace `AGENTS.md`.
|
|
174
|
-
- **One directory for the data**: `$DSH_HOME/dsh-agent-persona/` holds
|
|
175
|
-
`personas.json` and a `state.json` heartbeat. The data is re-read on every prompt
|
|
176
|
-
assembly (mtimeNs/size stamp), so an edit applies to the next message.
|
|
177
|
-
- **AI rework** (polish / expand / compress / draft) that uses the model DSH itself
|
|
178
|
-
is set to use, overridable per deployment with `tuneProvider` / `tuneModel`. It
|
|
179
|
-
only ever produces a proposal.
|
|
180
|
-
- **Zero runtime dependencies**: a plain ESM host half plus a hand-written
|
|
181
|
-
`__ModuleLoader__` client half (no build step), with 89 offline assertions in
|
|
182
|
-
`npm test`.
|
|
183
167
|
|
|
184
|
-
[
|
|
185
|
-
[0.1.0]: https://github.com/
|
|
168
|
+
[0.1.1]: https://github.com/Awoodwhale/dsh-agent-persona/compare/v0.1.0...v0.1.1
|
|
169
|
+
[0.1.0]: https://github.com/Awoodwhale/dsh-agent-persona/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/dsh-agent-persona)
|
|
6
6
|
[](./LICENSE)
|
|
7
|
-
[](https://github.com/Awoodwhale/dsh-agent-persona/actions/workflows/ci.yml)
|
|
8
8
|
|
|
9
9
|
[English](./README_EN.md)
|
|
10
10
|
|
|
@@ -22,17 +22,24 @@
|
|
|
22
22
|
- **追加或替换**:保留 DSH 自带的身份提示、把人设接在后面,或者去掉那句身份提示只用人设。
|
|
23
23
|
- **正文为空 = 这里不要人设**:给某个位置加一行、正文留空,就显式地在那里关掉人设。
|
|
24
24
|
- **改完下一条消息生效**,不用重启(正文按文件时间戳在每次组装时重读)。
|
|
25
|
+
- **工具改不动人设文件**:人设会被注入 system prompt,所以宿主注册了一个全局 `tools.guard`,拒绝一切指向人设
|
|
26
|
+
存储的写入(`write` / `edit` 各类文件工具,以及含写入特征的 shell 命令),只读命令照旧放行。
|
|
27
|
+
- **存储读不出来时不会悄悄丢**:文件存在但读不出来(内容坏了、不是人设存储)时,页面会说明原因,并且**在第一次
|
|
28
|
+
保存之前**先把原文件复制成 `personas.json.bak-<时间戳>`;备份失败就不会写入。
|
|
29
|
+
- **导入 / 导出**:导出为一份 JSON 文件(同时复制到剪贴板);导入可以选文件或粘贴,**边输入边预览**每条人设的
|
|
30
|
+
字数与规则数,再以「一律新建、一律停用」的方式写进去,默认人设不会被替换。
|
|
25
31
|
|
|
26
32
|
三处界面,各管一件事:
|
|
27
33
|
|
|
28
34
|
| 界面 | 位置 | 用途 |
|
|
29
35
|
|---|---|---|
|
|
30
|
-
| **设置页** | 设置 → Agent 人设 | 新建/编辑/删除/复制/排序人设;写正文、Markdown 预览、让 AI
|
|
36
|
+
| **设置页** | 设置 → Agent 人设 | 新建/编辑/删除/复制/排序人设;写正文、Markdown 预览、让 AI 改一版。标题旁另有 **GitHub / npm 链接**与**当前安装的版本号**(悬浮会说明这一份是 npm 装来的还是本地开发目录加载的) |
|
|
31
37
|
| **会话视图** | 对话页顶部的 **人设** 标签 | 看这条会话命中了哪条人设、**实际发出去的 system prompt**(渲染/源码两视图),以及就地管理全部人设 |
|
|
32
38
|
| **侧边栏卡片** | dsh-better-sidebar 的右侧栏 | 把上面那个「人设」页面挂进右侧栏(需要装了该插件) |
|
|
33
39
|
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
四个偏好开关(`编辑后自动保存` / `在对话页显示「人设」标签` / `注册到 dsh-better-sidebar` / `禁止模型改写人设文件`)
|
|
41
|
+
都在**设置 → Agent 人设**页里,并且这个页面**不受这些开关影响** —— 关掉「对话页显示」或「注册到 dsh-better-sidebar」后,
|
|
42
|
+
回到这里就能重新打开。本插件不在**设置 → 通用设置**里添加任何条目;鼠标停在开关上会给出每个开关的完整说明。
|
|
36
43
|
|
|
37
44
|
## 为什么它压得住 AGENTS.md
|
|
38
45
|
|
|
@@ -64,11 +71,19 @@ dsh plugin --profile web add dsh-agent-persona
|
|
|
64
71
|
|
|
65
72
|
仓库里也有本地安装脚本:`bash scripts/install.sh`(Windows 是 `scripts/install.ps1`)。
|
|
66
73
|
|
|
67
|
-
|
|
74
|
+
升级与卸载(`dsh plugin …` 就是在 profile 目录里代跑包管理器):
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
dsh plugin --profile web update dsh-agent-persona # 升级(按依赖的版本范围取最新)
|
|
78
|
+
dsh plugin --profile web add dsh-agent-persona@latest # 想跨大版本升到最新
|
|
79
|
+
dsh plugin --profile web remove dsh-agent-persona # 卸载
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
升级或卸载后**重启一次 `dsh web`**。人设文件不会被删、也不会被覆盖:卸载是人设只留在文件里的操作。
|
|
68
83
|
|
|
69
84
|
## 用起来
|
|
70
85
|
|
|
71
|
-

|
|
72
87
|
|
|
73
88
|
点 **新建人设** → 在「适用范围」里加行,选工作区或会话 → 写正文 → 打开启用开关 → 保存。
|
|
74
89
|
|
|
@@ -84,7 +99,7 @@ dsh plugin --profile web add dsh-agent-persona
|
|
|
84
99
|
|
|
85
100
|
展开一张卡是这样:
|
|
86
101
|
|
|
87
|
-

|
|
88
103
|
|
|
89
104
|
### 看这条会话聊了什么
|
|
90
105
|
|
|
@@ -98,7 +113,7 @@ dsh plugin --profile web add dsh-agent-persona
|
|
|
98
113
|
|
|
99
114
|
### 对话页的「人设」标签
|
|
100
115
|
|
|
101
|
-

|
|
102
117
|
|
|
103
118
|
选中一条会话后,对话页顶部有 **人设 / 提示词 / 管理** 三个面板:
|
|
104
119
|
|
|
@@ -109,13 +124,14 @@ dsh plugin --profile web add dsh-agent-persona
|
|
|
109
124
|
没有命中任何人设时,这里显示的就是 DSH 原生注入的 prompt —— 不做任何特殊处理。
|
|
110
125
|
- **管理**:与设置页同一套卡片(新建、启用/停用、编辑、排序、复制、删除),就地管理。
|
|
111
126
|
|
|
112
|
-
###
|
|
127
|
+
### 四个偏好
|
|
113
128
|
|
|
114
129
|
| 开关 | 默认 | 含义 | 生效时机 |
|
|
115
130
|
|---|---|---|---|
|
|
116
131
|
| 编辑后自动保存 | 关 | 停手约 1 秒自动保存编辑区内容 | 立即;切换到人设规则行(工作区/会话)时**不**自动保存,避免存下半截选择 |
|
|
117
132
|
| 在对话页显示「人设」标签 | 开 | 是否注册对话页那个视图 | 立即(运行时注册/注销) |
|
|
118
133
|
| 注册到 dsh-better-sidebar | 开 | 是否把「人设」页面注册进 dsh-better-sidebar 的右侧栏 | 立即;**没装该插件时这个开关不显示** |
|
|
134
|
+
| 禁止模型改写人设文件 | 开 | 拒绝模型的文件与命令工具写入或删除人设存储(读取不受影响);关掉后模型可以直接改它 | 立即;对**下一次工具调用**生效 |
|
|
119
135
|
|
|
120
136
|
偏好存在人设文件的 `prefs` 字段里(不是浏览器),所以它和数据的可见范围一致;浏览器里只留一份镜像,
|
|
121
137
|
供"加载时决定要不要注册视图"用。
|
|
@@ -188,12 +204,15 @@ AI 只产出建议稿,点「采用」或「追加」才会进编辑框,再
|
|
|
188
204
|
## 几句实话
|
|
189
205
|
|
|
190
206
|
- **人设是行为约束,不是安全边界**。它约束模型怎么说,不阻止模型用工具。
|
|
191
|
-
- `personas.json`
|
|
192
|
-
|
|
193
|
-
|
|
207
|
+
- `personas.json` 的防篡改是**行为层面**的:插件注册了全局 `tools.guard`,拒绝指向该文件的写入(文件工具看参数里
|
|
208
|
+
的路径,shell 命令看"是否提到该路径或文件名"且"是否带写入特征")。本机是 `danger-full-access` 时仍然可以绕过:
|
|
209
|
+
改插件代码、改 profile 的补丁文件,或者构造一条不带写入特征的载荷。要更结实就得在本机层面做:
|
|
210
|
+
`chflags uchg <人设文件>`(自己改之前先 `chflags nouchg`),或者把 DSH 跑在受限沙箱里、让人设文件属于别的用户。
|
|
211
|
+
- 那条 shell 判定**按整条命令**生效:把 `md5 <人设文件>` 和 `chmod <人设文件>` 写在同一行,整条都会被拒(设计如此)。
|
|
194
212
|
- 多处同时匹配时**只取第一条**,不拼接;规则是线性扫的,几十条没问题,几百条就该重新设计了。
|
|
195
213
|
- 一个 profile 里只装一次。同一个进程挂两个同 id 的行会让组合起不来。
|
|
196
|
-
- **测试的边界**:仓库里的测试是**单元测试 + 源码审计**(
|
|
214
|
+
- **测试的边界**:仓库里的测试是**单元测试 + 源码审计**(200 条宿主断言 + 165 条客户端断言 + 生成产物漂移校验;
|
|
215
|
+
这个数字由测试自己统计,不会随文件改动而失真),
|
|
197
216
|
它们保证逻辑与代码形状,**不驱动浏览器** —— 界面行为(视图渲染、开关、侧边栏卡片)目前靠人工核对。
|
|
198
217
|
|
|
199
218
|
## 开发
|
|
@@ -213,7 +232,7 @@ docs/ 架构、开发笔记、设计决策
|
|
|
213
232
|
npm install
|
|
214
233
|
npm run build # generate:remote → tsc → esbuild(产出 lib/)
|
|
215
234
|
npm run typecheck # 两个 tsconfig 都不报错
|
|
216
|
-
npm test #
|
|
235
|
+
npm test # 200 + 165 条断言 + 生成产物漂移校验
|
|
217
236
|
dsh --profile web --dump-config | grep -c 'id: agent-persona' # 组合自检,应输出 1
|
|
218
237
|
```
|
|
219
238
|
|
package/README_EN.md
CHANGED
|
@@ -4,7 +4,7 @@ Different system-prompt personas for different **workspaces** and different **se
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/dsh-agent-persona)
|
|
6
6
|
[](./LICENSE)
|
|
7
|
-
[](https://github.com/Awoodwhale/dsh-agent-persona/actions/workflows/ci.yml)
|
|
8
8
|
|
|
9
9
|
[中文](./README.md)
|
|
10
10
|
|
|
@@ -28,18 +28,27 @@ wants its own identity, tone and constraints. This plugin hands each of them its
|
|
|
28
28
|
persona.
|
|
29
29
|
- **Empty text means "no persona here"** — add a rule for a place and leave the body empty to switch it off there.
|
|
30
30
|
- **An edit applies to the next message.** No restart: the body is re-read per assembly by file stamp.
|
|
31
|
+
- **Tools cannot write the persona file.** The body becomes part of the system prompt, so the host registers a
|
|
32
|
+
global `tools.guard` that refuses every call pointing at the persona store — file tools by the paths in their
|
|
33
|
+
arguments, shell commands by mentioning the store together with a write feature — while read-only commands pass.
|
|
34
|
+
- **A store that cannot be read is never lost quietly.** When the file exists but cannot be used, the page says
|
|
35
|
+
what happened and why, and the first save copies the original to `personas.json.bak-<timestamp>` before touching
|
|
36
|
+
it. If that copy fails, the write fails with it.
|
|
37
|
+
- **Import / export.** Export writes a JSON file (and copies it to the clipboard); import takes a chosen file or a
|
|
38
|
+
pasted document, **previews as you type** every persona with its size and reach, and creates them **disabled**
|
|
39
|
+
and appended at the end, never taking over the default slot.
|
|
31
40
|
|
|
32
41
|
Three surfaces, three jobs:
|
|
33
42
|
|
|
34
43
|
| Surface | Where | What for |
|
|
35
44
|
|---|---|---|
|
|
36
|
-
| **Settings page** | Settings → Agent 人设 | Create, edit, delete, duplicate and reorder personas; write the body, preview Markdown, let AI rework it |
|
|
45
|
+
| **Settings page** | Settings → Agent 人设 | Create, edit, delete, duplicate and reorder personas; write the body, preview Markdown, let AI rework it. Beside the title: the **GitHub and npm links** and the **version of the copy actually installed** (its tooltip says whether this copy came from npm or from a local checkout) |
|
|
37
46
|
| **Session view** | the **人设** tab on a conversation | Which persona this session matched and why, the **system prompt it actually sent** (rendered and source), and the same management UI inline |
|
|
38
47
|
| **Sidebar card** | the right bar of `dsh-better-sidebar` | Puts that same view into the sidebar (needs that plugin installed) |
|
|
39
48
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
49
|
+
All three preference switches (`编辑后自动保存` / `在对话页显示「人设」标签` / `注册到 dsh-better-sidebar`) live on the
|
|
50
|
+
**Settings → Agent 人设** page, which none of them can hide: switching the conversation tab or the sidebar card
|
|
51
|
+
off and returning there is how you turn them back on. This plugin adds nothing to **Settings → General**.
|
|
43
52
|
|
|
44
53
|
## Why it outranks AGENTS.md
|
|
45
54
|
|
|
@@ -72,11 +81,21 @@ After the restart:
|
|
|
72
81
|
where the store is.
|
|
73
82
|
|
|
74
83
|
Local install scripts live in `scripts/install.sh` and `scripts/install.ps1`.
|
|
75
|
-
|
|
84
|
+
|
|
85
|
+
Upgrading and removing (`dsh plugin …` runs the profile's package manager for you):
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
dsh plugin --profile web update dsh-agent-persona # upgrade within the declared range
|
|
89
|
+
dsh plugin --profile web add dsh-agent-persona@latest # jump to the newest major
|
|
90
|
+
dsh plugin --profile web remove dsh-agent-persona # uninstall
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Restart `dsh web` once** after an upgrade or a removal. Your persona file is neither deleted nor
|
|
94
|
+
overwritten — uninstalling only removes the plugin.
|
|
76
95
|
|
|
77
96
|
## Using it
|
|
78
97
|
|
|
79
|
-

|
|
80
99
|
|
|
81
100
|
**New persona** → add rows under reach, pick a workspace or a session → write the body → switch it on → save.
|
|
82
101
|
|
|
@@ -88,6 +107,10 @@ To remove: `dsh plugin --profile web remove dsh-agent-persona`, restart; the per
|
|
|
88
107
|
- Boundaries like prefixes and regexes are available through **type it yourself…**.
|
|
89
108
|
- A persona with **no rows applies nowhere** (it is a draft) unless it is marked as the default.
|
|
90
109
|
|
|
110
|
+
An expanded card — name, state, the default switch, injection mode, the reach rules, the body and the footer:
|
|
111
|
+
|
|
112
|
+

|
|
113
|
+
|
|
91
114
|
### Reading what a session actually said
|
|
92
115
|
|
|
93
116
|
The button next to the session dropdown opens a **read-only** transcript: your inputs on the right, the agent's
|
|
@@ -102,7 +125,7 @@ replies on the left.
|
|
|
102
125
|
|
|
103
126
|
### The 人设 tab on a conversation
|
|
104
127
|
|
|
105
|
-

|
|
106
129
|
|
|
107
130
|
**人设 / 提示词 / 管理**:
|
|
108
131
|
|
|
@@ -113,13 +136,14 @@ replies on the left.
|
|
|
113
136
|
persona matched it shows DSH's own prompt verbatim.
|
|
114
137
|
- **管理** — the same cards as the settings page, in place.
|
|
115
138
|
|
|
116
|
-
### The
|
|
139
|
+
### The four preferences
|
|
117
140
|
|
|
118
141
|
| Switch | Default | Meaning | Takes effect |
|
|
119
142
|
|---|---|---|---|
|
|
120
143
|
| 编辑后自动保存 | off | Save the editor about a second after typing stops | Immediately; rule rows (workspace/session) deliberately do **not** autosave, so a half-made choice is never stored |
|
|
121
144
|
| 在对话页显示「人设」标签 | on | Register the conversation view or not | Immediately (registered/unregistered at runtime) |
|
|
122
145
|
| 注册到 dsh-better-sidebar | on | Register the card with the sidebar plugin | Immediately; **not shown at all when that plugin is absent** |
|
|
146
|
+
| 禁止模型改写人设文件 | on | Refuse the model's file and shell tools any write to or removal of the persona store (reads still work); with it off the model can edit it directly | Immediately, on the next tool call |
|
|
123
147
|
|
|
124
148
|
Preferences live in the persona file's `prefs` field — not in the browser — so they share the data's scope. The
|
|
125
149
|
browser keeps only a mirror, used to decide at load whether to register the views.
|
|
@@ -189,14 +213,20 @@ Agent 人设 page (`保存在 …`), so it is never a mystery which file you are
|
|
|
189
213
|
|
|
190
214
|
- **A persona is a behavioural constraint, not a security boundary.** It shapes what the model says; it does not
|
|
191
215
|
stop tools.
|
|
192
|
-
-
|
|
193
|
-
|
|
194
|
-
|
|
216
|
+
- Tamper protection is a **behaviour** measure: the plugin registers a global `tools.guard` that refuses writes to
|
|
217
|
+
`personas.json` (file tools by the paths in their arguments; shell commands by mentioning the store together
|
|
218
|
+
with a write feature). On a `danger-full-access` machine it can still be bypassed — edit the plugin, edit the
|
|
219
|
+
profile patch, or craft a payload without a write marker. Harder options live at the machine level:
|
|
220
|
+
`chflags uchg <store>` (run `chflags nouchg` before editing it yourself), or a restricted sandbox with the store
|
|
221
|
+
owned by another user.
|
|
222
|
+
- That shell test reads the **whole command**: a `md5 <store>` and a `chmod <store>` on one line are refused as a
|
|
223
|
+
unit (deliberate).
|
|
195
224
|
- Only the **first** match is used — no concatenation. Matching is a linear scan; dozens of rules are fine,
|
|
196
225
|
hundreds want a redesign.
|
|
197
226
|
- Install once per profile: two rows with the same id in one process make the composition fail.
|
|
198
|
-
- **Test boundary**: the suite is unit tests plus source audits (
|
|
199
|
-
generated-artifact drift check
|
|
227
|
+
- **Test boundary**: the suite is unit tests plus source audits (200 host assertions, 165 client assertions, and a
|
|
228
|
+
generated-artifact drift check; the numbers are counted by the suites themselves, so they cannot fall behind
|
|
229
|
+
the files). It does not drive a browser — UI behaviour is checked by hand today.
|
|
200
230
|
|
|
201
231
|
## Development
|
|
202
232
|
|
|
@@ -215,7 +245,7 @@ docs/ architecture, development notes, design decisions
|
|
|
215
245
|
npm install
|
|
216
246
|
npm run build # generate:remote → tsc → esbuild (writes lib/)
|
|
217
247
|
npm run typecheck # both tsconfigs, no errors
|
|
218
|
-
npm test #
|
|
248
|
+
npm test # 200 + 165 assertions plus the generated-artifact drift check
|
|
219
249
|
dsh --profile web --dump-config | grep -c 'id: agent-persona' # composition self-check, expects 1
|
|
220
250
|
```
|
|
221
251
|
|