dsh-agent-persona 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,44 +4,158 @@ 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
- ## [Unreleased]
7
+ ## [0.1.2] - 2026-09-21
8
+
9
+ ### Fixed
10
+
11
+ - **The sidebar card could be missing after a restart even though the switch showed it on.** The two read
12
+ different sources: the switch shows the store's `prefs.showSidebar` (through the remote view), while the
13
+ registration resolved the sidebar service once, when this plugin applied, and gave up for good if it was not
14
+ there yet. Plugin load order is not guaranteed, so a restart could put this plugin's client half before the
15
+ sidebar plugin's; the card then only came back by toggling the switch by hand. The service is now awaited —
16
+ registered immediately when it is already present, otherwise through `ctx.inject(['betterSidebar'], …)` and
17
+ bound to the arriving context, so it is torn down with the provider. The switch still acts at once.
18
+
19
+ ### Added
20
+
21
+ - A `screenshots.json` in the repository, so storefronts show the three interface screenshots and they can be
22
+ replaced by pushing here rather than by opening a pull request elsewhere.
23
+
24
+ ## [0.1.1] - 2026-09-20
8
25
 
9
26
  ### Added
10
27
 
28
+ - **A readable store is protected before it is replaced.** A store file that exists but cannot be read used to
29
+ look exactly like an empty one, and the next save replaced it. The page now says what happened and why, and
30
+ the first write copies the original to `personas.json.bak-<timestamp>` before touching it. If the copy fails
31
+ the write fails with it, so content nothing preserved is never destroyed.
32
+ - **The persona store is no longer writable by tools.** A monotonic `ctx.tools.guard` refuses any call whose
33
+ arguments point at the store and any shell command that mentions it together with a write feature
34
+ (`>`, `rm`, `mv`, `cp`, `chmod`, `sed -i`, `tee`, `truncate`, …), while reads (`cat`, `md5`, …) stay allowed.
35
+ A session running under a persona can therefore no longer rewrite or delete that persona. It is a
36
+ **preference** (`禁止模型改写人设文件`), on by default, and the guard reads it live so a toggle applies to the
37
+ next tool call. It is a behavioural limit, not a security boundary, and the switch says so on hover.
38
+ - **导入 / 导出.** Export writes a JSON file (and copies it to the clipboard); import takes a chosen file or a
39
+ pasted document, **previews as you type** every persona with its size and reach, and creates them through the
40
+ same `savePersona` call the page uses, so a document cannot bypass the host's validation. Imported personas are
41
+ always created **disabled** and appended at the end, and the default slot is never taken over. The dialog
42
+ colours the JSON in place, draws its own thin scrollbar, and offers the replace-everything restore behind a
43
+ switch that names the count it would delete.
44
+ - **A release workflow** (`.github/workflows/release.yml`): publishing a GitHub Release whose tag matches
45
+ `package.json` runs the same build/typecheck/test gates as CI and publishes to npm through Trusted Publishing
46
+ (OIDC) — no `NPM_TOKEN`, provenance attached, prereleases going to the `next` dist-tag.
47
+ - Source links and the running version beside the settings title: GitHub and npm as two small pills carrying the
48
+ real brand marks (inlined from simple-icons, `currentColor`, no hardcoded colours), plus the version of the
49
+ copy actually running. The version comes from the host reading its own `package.json`, so a checkout and an npm
50
+ install report different things — and the tooltip says which one this is. The same two fields go into the
51
+ load heartbeat.
52
+
53
+ ### Changed
54
+
55
+ - **The header spacing follows the host's own rhythm.** The title, description and counts now sit on a uniform
56
+ 12px gap, and the extra 10px margin on the preference row is gone: the counts, the switches and the cards are
57
+ 14px apart, the distance the settings shell itself uses between blocks.
58
+ - The reported test counts are counted rather than written down: both suites wrap `assert` and report what ran,
59
+ so the number can no longer fall behind the file (200 host assertions, 165 client assertions).
60
+ - **Opening and closing a card is one CSS transition**, driven by the class the card already carries: a grid row
61
+ interpolating between `0fr` and `1fr`, over one easing and one duration in both directions. Nothing is timed,
62
+ nothing is unmounted, and no transform is involved, so a fixed-position tooltip can never be captured by it.
63
+ The scope line under the header collapses in the opposite direction over the same span — it used to be mounted
64
+ and unmounted with the state, which made the whole card jump by its height the moment a card was opened.
65
+ - The card's chevron turns exactly **90°**: it keeps one glyph, rotated by CSS. Swapping in a down-chevron as
66
+ well turned it twice, so an open card pointed the wrong way.
67
+ - **Each preference explains itself on hover.** The switches used to carry their own label as a native tooltip,
68
+ which explained nothing: `保护人设文件` became `禁止模型改写人设文件`, and every switch now has a real tooltip
69
+ (also folded into its accessible name) saying what it does and what it does not.
70
+ - **Nothing is added to Settings → General.** The switches live on the Agent 人设 page alone, which none of them
71
+ can hide, so that page is the way back from any of them.
72
+
73
+ ### Fixed
74
+
75
+ - A note about a preserved store no longer disappears the moment it becomes true: a reload only clears the
76
+ record for a *fresh* reset, not for the stamp change caused by our own write.
77
+ - **Tooltips opened inside the view or the sidebar card landed in the wrong place.** The entry animations filled
78
+ forwards, and a transform keyframe that ends at `transform: none` still leaves an identity matrix — which
79
+ counts as a transform, and therefore made the panel the containing block for the UI kit's fixed-position
80
+ bubble. Every animation now fills backwards, so the final state is the element's own style, and reduced-motion
81
+ covers the new ones too.
82
+ - **`scripts/install.sh` and `scripts/install.ps1` were rewritten**: they still carried the pre-rename package
83
+ name, the old heartbeat path and the old page title. They now install `dsh-agent-persona`, print the version of
84
+ the checkout, state the real store and heartbeat paths, offer upgrade and removal, and refuse a path target
85
+ that does not exist — the CLI writes a `link:` dependency for whatever it is given, so a typo silently became a
86
+ broken profile entry.
87
+
88
+ ## [0.1.0] - 2026-09-17
89
+
90
+ ### Added
91
+
92
+ - **Many personas, each scoped independently.** A persona is a first-class record
93
+ (`id` / `name` / `enabled` / `text` / `targets[]`), and every target is
94
+ `{ kind: 'workspace' | 'sessionId', match: 'exact' | 'prefix' | 'regex' | 'contains', value }`.
95
+ - **An explicit default persona** (`默认人设`) with a `fallback` flag: it takes every session no reach rule
96
+ claimed, and only one persona can hold it — marking a new one releases the previous.
97
+ - **Dropdown scope pickers.** The settings page fills its pickers from the host's
98
+ workspaces and from DSH's own sessions (newest first, with title, working
99
+ directory and a relative timestamp), so nobody has to type a path or a session
100
+ id. The two kinds are mutually exclusive per row — switching a row's type clears
101
+ its value — and "自己输入…" keeps the advanced match modes reachable.
102
+ - **System-prompt injection** as section `agent-persona` at order 1
103
+ (`DEPLOYMENT_PERSONA_PREFIX + 1`), i.e. above the workspace `AGENTS.md`, with
104
+ `append` (default) and `replace` injection modes.
11
105
  - **A 人设 tab on the conversation page**: which persona the session matched, the system prompt it actually sent
12
106
  (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
107
  - **A sidebar card** for `dsh-better-sidebar`, registered through that plugin's own service. The switch that
18
108
  controls it is **not shown at all** when that plugin is absent.
109
+ - **Interface preferences** — `编辑后自动保存`, `在对话页显示「人设」标签`, `注册到 dsh-better-sidebar` — stored in the
110
+ persona file's `prefs` field, so they share the data's scope; the browser keeps only a load-time mirror.
111
+ - **Session history**: pick a session, hit the button next to the picker, and read that conversation. Reads are
112
+ windowed (`sessionHistory({id, offset, events})`) so the log is never read up front: the page shows what it
113
+ got, reports 已显示 N 条, and only reads the next window when 继续加载 is pressed. Messages are capped at 4000
114
+ characters, and 展开全文 replaces a single bubble by reading that one event again (`offset: at, events: 1`).
115
+ - **AI rework** (polish / expand / compress / draft) that uses the model DSH itself
116
+ is set to use, overridable per deployment with `tuneProvider` / `tuneModel`. It
117
+ only ever produces a proposal.
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.
19
122
  - **A Schemastery config**: `storePath`, `tuneProvider`, `tuneModel`, validated when the row loads.
20
123
  - **TypeScript sources with a build**: `src/**` compiles to `lib/`, and a generator derives the remote artifacts
21
124
  (`src/remote.ts`, `src/typert.ts`) from the single endpoint list in `src/endpoints.ts`; `npm test` fails if the
22
125
  generated files drift.
126
+ - **One directory for the data**: `$DSH_HOME/dsh-agent-persona/` holds
127
+ `personas.json` and a `state.json` heartbeat. The data is re-read on every prompt
128
+ assembly (mtimeNs/size stamp), so an edit applies to the next message.
129
+ - **Zero runtime dependencies** for the host: the client half is bundled by esbuild, the host half is plain ESM,
130
+ and `npm test` runs both offline suites.
23
131
 
24
132
  ### Changed
25
133
 
26
- - **A persona with no reach applies nowhere.** It used to act as a catch-all; a place-less persona is a draft now,
27
- and where nothing matched the harness prompt stands. `默认人设` is the explicit way to ask for a fallback.
134
+ - **Resolution is specificity-first.** A session-id rule outranks a workspace rule and an exact value outranks a
135
+ pattern (`targetSpecificity`); list order only breaks ties between equally specific rules. A persona pointed at
136
+ one exact session therefore beats a persona pointed at the whole directory, whatever the order.
137
+ - **A persona with no reach applies nowhere.** A place-less persona is a draft, and where nothing matched the
138
+ harness prompt stands; `默认人设` is the explicit way to ask for a fallback.
28
139
  - **Replace mode also drops the harness identity line**, not only the deployment's persona line.
140
+ - **Exactly one persona per workspace or session.** A save that claims a place takes it away from the persona
141
+ that held it (exact values only, reported back to the page), and the pickers label places that are already
142
+ taken.
143
+ - The collapsed card keeps only what reads at a glance — chevron, state dot, name, state, scope chips — and
144
+ reordering / duplicating / deleting moved into the card's `⋯` menu.
145
+ - The status line is sticky at the bottom of the page so save/error feedback stays visible while the list
146
+ scrolls; icon buttons grew from 24px to 28px hit targets.
147
+ - The session picker shows how often each session was talked to (`sessionStats.turns`), beside the short id and
148
+ age, and sessions are labelled with their DSH title or their first prompt.
149
+ - Renamed the package from `dsh-workspace-persona` to **`dsh-agent-persona`**. Everything it writes now lives in
150
+ `$DSH_HOME/dsh-agent-persona/`.
29
151
  - Display preferences take effect immediately rather than on the next page load: the conversation view and the
30
152
  sidebar card are registered and unregistered at runtime.
31
153
 
32
154
  ### Fixed
33
155
 
34
- - The remote manifest is generated in the shape the mount consumes: an `id` per descriptor, a strict input codec,
35
- `parameters: []` for the reads that take none, and the service name `agentPersona`.
36
- - The default flag survives a store round trip again — the reader no longer drops it — and a save whose payload
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
-
156
+ - **跳到最新 did nothing**: the runtime forwarded a hand-written subset of the history options and dropped
157
+ `tail` / `keep`, so the call degraded to a normal head read while the dialog labelled it as the tail. The
158
+ forwarding is now an exported, asserted `historyOptions`.
45
159
  - 跳到最新 kept a fixed four-message window, so a turn where the agent answered several times pushed the user's
46
160
  question out of it. The tail now returns everything from the **last user input** onwards (bounded at 60
47
161
  messages while walking).
@@ -49,137 +163,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
49
163
  bubbles show raw Markdown. Messages are now split into bounded Markdown-safe chunks (blank-line boundaries,
50
164
  never inside a code fence) and each chunk renders on its own: one awkward chunk degrades with a visible note,
51
165
  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
166
  - **Sessions could not be read at all**: `sessionPersistence.open(id, access)` takes the access mode as a
84
167
  second, required argument; the reader called `open(id)` and every candidate failed, so the conversation
85
168
  dialog always reported "no reader". It now opens with `'read'` (never taking write ownership) and retries
86
169
  id-only signatures for version tolerance.
87
-
88
- ### Changed
89
-
90
- - **Resolution is specificity-first.** A session-id rule now outranks a workspace rule and an exact value
91
- outranks a pattern (`targetSpecificity`); list order only breaks ties between equally specific rules. A persona
92
- pointed at one exact session therefore beats a persona pointed at the whole directory, whatever the order.
93
-
94
- - The session picker shows how often each session was talked to (`sessionStats.turns` from the same
95
- projection read as the title), beside the short id and age; picker menus are widened to fit that row.
96
-
97
- - The settings section paints its title and description (and a skeleton card list) before any host call
98
- resolves, instead of showing a bare 「加载中…」; a mount race with the remote namespace retries briefly rather
99
- than flashing an error.
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
170
+ - **Empty sessions are no longer offered.** A session created with a preset but never typed into
171
+ (`sessionListMetadata.blank`, the same flag the sidebar hides on) holds no conversation to give a persona, so
172
+ the picker drops it and counts it in the hidden tally.
173
+ - **Sub-agent and archived sessions no longer appear in the picker.** A sub-agent session (spawned by the
174
+ model: `origin: 'subagent'` or `delegationDepth > 0`) and an archived one cannot be opened or typed into from
175
+ the sidebar, so listing them invited personas that could never apply. The page reports how many were hidden.
176
+ - The remote manifest is generated in the shape the mount consumes: an `id` per descriptor, a strict input codec,
177
+ `parameters: []` for the reads that take none, and the service name `agentPersona`.
178
+ - The default flag survives a store round trip again — the reader no longer drops it — and a save whose payload
179
+ omits the flag keeps the stored value instead of clearing it.
180
+ - The session view's own draft and dirty check cover the default flag, and its switch no longer calls a method
181
+ that only the settings component has. Together those made the switch look inert and let a save turn it off.
182
+ - The conversation dialog hides plugin-injected user-side content — `AGENTS.md`, runtime snapshots, the skill
149
183
  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
184
 
184
- [Unreleased]: https://github.com/awoodwhale/dsh-agent-persona/compare/v0.1.0...HEAD
185
- [0.1.0]: https://github.com/awoodwhale/dsh-agent-persona/releases/tag/v0.1.0
185
+ [0.1.2]: https://github.com/Awoodwhale/dsh-agent-persona/compare/v0.1.1...v0.1.2
186
+ [0.1.1]: https://github.com/Awoodwhale/dsh-agent-persona/compare/v0.1.0...v0.1.1
187
+ [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
  [![npm](https://img.shields.io/npm/v/dsh-agent-persona)](https://www.npmjs.com/package/dsh-agent-persona)
6
6
  [![license](https://img.shields.io/badge/license-Apache--2.0-blue)](./LICENSE)
7
- [![ci](https://github.com/awoodwhale/dsh-agent-persona/actions/workflows/ci.yml/badge.svg)](https://github.com/awoodwhale/dsh-agent-persona/actions/workflows/ci.yml)
7
+ [![ci](https://github.com/Awoodwhale/dsh-agent-persona/actions/workflows/ci.yml/badge.svg)](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
- 三个偏好开关(`编辑后自动保存` / `在对话页显示「人设」标签` / `注册到 dsh-better-sidebar`)在
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
- 卸载:`dsh plugin --profile web remove dsh-agent-persona`,重启即可;人设文件不会被删。
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
- ![人设列表](./docs/images/settings-list.png)
86
+ ![人设列表](https://raw.githubusercontent.com/Awoodwhale/dsh-agent-persona/main/docs/images/settings-list.png)
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
- ![展开编辑](./docs/images/settings-editor.png)
102
+ ![展开编辑](https://raw.githubusercontent.com/Awoodwhale/dsh-agent-persona/main/docs/images/settings-editor.png)
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
- ![会话视图](./docs/images/view-persona.png)
116
+ ![会话视图](https://raw.githubusercontent.com/Awoodwhale/dsh-agent-persona/main/docs/images/view-persona.png)
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
- `tools.guard`。真要「工作区里的会话改不动人设」,得在本机层面做:`chmod 400` + 换个属主,
193
- 或者把 DSH 跑在受限沙箱里。
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
- - **测试的边界**:仓库里的测试是**单元测试 + 源码审计**(134 条宿主断言 + 15 条客户端断言 + 生成产物漂移校验),
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 # 134 + 15 条断言 + 生成产物漂移校验
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
  [![npm](https://img.shields.io/npm/v/dsh-agent-persona)](https://www.npmjs.com/package/dsh-agent-persona)
6
6
  [![license](https://img.shields.io/badge/license-Apache--2.0-blue)](./LICENSE)
7
- [![ci](https://github.com/awoodwhale/dsh-agent-persona/actions/workflows/ci.yml/badge.svg)](https://github.com/awoodwhale/dsh-agent-persona/actions/workflows/ci.yml)
7
+ [![ci](https://github.com/Awoodwhale/dsh-agent-persona/actions/workflows/ci.yml/badge.svg)](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
- Three preference switches (`编辑后自动保存` / `在对话页显示「人设」标签` / `注册到 dsh-better-sidebar`) appear both on the
41
- manage page and in **Settings → General** — the latter is not affected by them, so a display position can always
42
- be switched back on.
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
- To remove: `dsh plugin --profile web remove dsh-agent-persona`, restart; the persona file is left alone.
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
- ![persona list](./docs/images/settings-list.png)
98
+ ![persona list](https://raw.githubusercontent.com/Awoodwhale/dsh-agent-persona/main/docs/images/settings-list.png)
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
+ ![expanded card](https://raw.githubusercontent.com/Awoodwhale/dsh-agent-persona/main/docs/images/settings-editor.png)
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
- ![session view](./docs/images/view-persona.png)
128
+ ![session view](https://raw.githubusercontent.com/Awoodwhale/dsh-agent-persona/main/docs/images/view-persona.png)
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 three preferences
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
- - `personas.json` is an **ordinary file**: any session with file tools can write it. This plugin does **not**
193
- register a `tools.guard`. If you need "a conversation inside the workspace cannot change the persona", do it at
194
- the machine level (`chmod 400` plus a different owner, or a restricted sandbox).
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 (134 host assertions, 15 client assertions, and a
199
- generated-artifact drift check). It does not drive a browser — UI behaviour is checked by hand today.
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 # 134 + 15 assertions plus the generated-artifact drift check
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