@flotiarenor/dsh-tool-text-editor 1.1.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +191 -117
- package/README.zh.md +67 -137
- package/lib/core.mjs +341 -375
- package/lib/editor.mjs +131 -154
- package/lib/mask.mjs +440 -0
- package/package.json +19 -13
- package/preset/preset.yml +1 -1
- package/scripts/install-preset.mjs +127 -74
package/README.md
CHANGED
|
@@ -2,45 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
[中文](README.zh.md) | English
|
|
4
4
|
|
|
5
|
-
Model-facing tools for [DeepSeek Harness](https://github.com/deepseek-ai) (dsh) that edit text
|
|
6
|
-
|
|
5
|
+
Model-facing tools for [DeepSeek Harness](https://github.com/deepseek-ai) (dsh) that edit text files
|
|
6
|
+
**byte-faithfully**: `edit_text` and `write_text`. They fix what the built-in `write` / `edit`
|
|
7
|
+
(`@deepseek-ai/dsh-fs-local`) cannot do — both defects below were verified against a real installation
|
|
8
|
+
(§ "Versus the built-ins"):
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
|
11
|
-
|
|
12
|
-
|
|
|
13
|
-
| full overwrite | — | **BOM lost + CRLF flattened to LF** | BOM + CRLF kept |
|
|
14
|
-
|
|
15
|
-
`@deepseek-ai/dsh-fs-local` has no BOM handling at all (Node's `TextDecoder` strips a leading BOM
|
|
16
|
-
byte by default) and `writeText` does not restore a file's line-ending style.
|
|
10
|
+
| Built-in behaviour | Cause | This plugin |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| **UTF-8 BOM lost** on any edit or overwrite | the implementation has no BOM handling; Node's `TextDecoder` strips a leading BOM by default | BOM preserved |
|
|
13
|
+
| **Line-ending style not restored** on a full overwrite | `writeText` writes `content` verbatim, so LF content turns a CRLF file into an LF file | line endings follow the file |
|
|
14
|
+
| **`FS_EDIT_NOT_FOUND`** when `old_string` differs by a space | the built-in `edit` matches literally, with no fallback | exact → relaxed → nearest candidates (ambiguity refuses to write) |
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
refusal**, and **near-miss candidates** when an anchor does not match.
|
|
16
|
+
Also **`grep` / `lines` anchors** (old text is never copied by hand) and **near-miss candidates**;
|
|
17
|
+
"Return value" documents the result value and the model-facing text.
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
projection are documented under "Return value".
|
|
19
|
+
A call leaves **no side artifacts**: the only thing written is the target file.
|
|
24
20
|
|
|
25
21
|
## Implementation and requirements
|
|
26
22
|
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
**In-process Node** (`lib/core.mjs`): `node:` builtins only, no subprocess, no build step, no third-party
|
|
24
|
+
package.
|
|
29
25
|
|
|
30
26
|
| Requirement | Notes |
|
|
31
27
|
|---|---|
|
|
32
|
-
| Node | **The only dependency
|
|
28
|
+
| Node | **The only dependency**: no interpreter, no external runtime, no process-startup cost per call. |
|
|
33
29
|
|
|
34
|
-
The package installs nothing
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
The package installs nothing. Its one `peerDependencies` entry, `@deepseek-ai/dsh-tools`, is the host
|
|
31
|
+
contract ("needs this dsh, and not a later major line") and resolves from the dsh installation, not beside
|
|
32
|
+
the plugin. The range carries two clauses, `>=0.1.0-rc.6 || >=0.1.5-rc.2`, because npm admits a prerelease
|
|
33
|
+
only when the range has a comparator sharing its `major.minor.patch` tuple; each verified dsh line therefore
|
|
34
|
+
needs its own clause, or a prerelease install reads as an unmet peer.
|
|
37
35
|
|
|
38
36
|
## Install
|
|
39
37
|
|
|
40
38
|
### Option 1 — preset (recommended, tightly scoped)
|
|
41
39
|
|
|
42
|
-
Only sessions
|
|
43
|
-
repository:
|
|
40
|
+
Only sessions selecting this preset see the two tools. Run from the root of a clone of this repository:
|
|
44
41
|
|
|
45
42
|
```powershell
|
|
46
43
|
node scripts/install-preset.mjs
|
|
@@ -48,13 +45,13 @@ node scripts/install-preset.mjs
|
|
|
48
45
|
# / --force / --dry-run / --from <agent.cordis.yml path>
|
|
49
46
|
```
|
|
50
47
|
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
Restart `dsh web` and start a new session on preset `texteditor`: a preset is a session-creation fact, so a
|
|
49
|
+
running session cannot switch to it.
|
|
53
50
|
|
|
54
51
|
### Option 2 — install into the profile (available to every session)
|
|
55
52
|
|
|
56
|
-
`dsh plugin add` accepts several kinds of source,
|
|
57
|
-
|
|
53
|
+
`dsh plugin add` accepts several kinds of source, all working here: the package is prebuilt, dependency-free
|
|
54
|
+
ESM, with no `prepare` / `build` step to authorize or run.
|
|
58
55
|
|
|
59
56
|
| Source | Command |
|
|
60
57
|
|---|---|
|
|
@@ -69,85 +66,138 @@ Verify the layer without starting anything, then restart:
|
|
|
69
66
|
dsh --profile web --dump-config # look for the "# == @flotiarenor/dsh-tool-text-editor" layer
|
|
70
67
|
```
|
|
71
68
|
|
|
72
|
-
The tool names do not collide with the built-ins, so a host-plane insert is safe
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
The tool names do not collide with the built-ins, so a host-plane insert is safe; the cost is both tools and
|
|
70
|
+
their guidance section in **every** session. Uninstall with `dsh plugin --profile web remove @flotiarenor/dsh-tool-text-editor`.
|
|
71
|
+
Both installs may coexist, the preset layer shadowing the host layer with an identical definition.
|
|
72
|
+
|
|
73
|
+
## Masking the built-in `write` / `edit`
|
|
74
|
+
|
|
75
|
+
The editor row **adds** `edit_text` / `write_text` and leaves the built-ins alone. Removing them is a second,
|
|
76
|
+
separate row — `lib/mask.mjs` — that the installer only injects on request:
|
|
77
|
+
|
|
78
|
+
```powershell
|
|
79
|
+
node scripts/install-preset.mjs --mask-native # also injects the tool-native-edit-mask row (and guidance: short)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Two rows are forced, not a matter of taste: `tools.restrict()` is callable only from a **scoped context**
|
|
83
|
+
(`agent.ctx`), and a preset row's own context *is* the standing scope where the natives are registered, so a row
|
|
84
|
+
cannot hide tools from itself. Masking is therefore a fact about the composition, and only a second row can state
|
|
85
|
+
it.
|
|
86
|
+
|
|
87
|
+
It removes **2362 B per request** (`node tools/measure-context.mjs --vs-native`): two schemas (1754 B: `write`
|
|
88
|
+
728 + `edit` 1026, sandbox escalation fields included) plus two guidance sections (608 B: 220 + 388);
|
|
89
|
+
`guidance: short` on the editor row removes another 81 B (241 -> 160), for **2443 B per request** (~600 tokens).
|
|
90
|
+
|
|
91
|
+
| Mechanism | How it works |
|
|
92
|
+
| --- | --- |
|
|
93
|
+
| Guard, armed in `apply()` on the row's own scope layer | Refuses the first native call of any agent, naming `edit_text` / `write_text`, and narrows that agent in the same call, so the next request's table is already clean. Being on the layer rather than on an event, its coverage does not depend on when the agent was created |
|
|
94
|
+
| Tool catalog and execution | `agent.ctx.tools.restrict({ deny: ['write','edit'] })` — the named tools leave the catalog **and** become uncallable (naming one yields `UNKNOWN_TOOL`) |
|
|
95
|
+
| Prompt | `agent.ctx.systemPrompt.section({ name: 'tool:write', text: '' })` shadows the two guidance sections `dsh-tool-fs` registers. A redundant safety net: since 0.1.5-rc.2 those sections are visibility-gated and disappear on their own |
|
|
96
|
+
| Membership | The guard is asked who it belongs to, because a preset can be swapped under a live agent. The row calls `ctx.tools.guardReason()` with an execution object **it minted itself**, so the answer is object identity, not a name: a real dispatch always mints a fresh object and can never be mistaken for the probe, and a foreign guard that refuses everything cannot answer for this row |
|
|
97
|
+
| Leaving, and unloading | `restrict` and the shadowing sections sit on the **agent's own layer**, so they outlive the preset; each is kept as a disposer and lifted when the agent re-links elsewhere — otherwise that agent is left with neither the native names nor this plugin's tools, i.e. no way to write at all. Unloading the row does the same for every agent it masked |
|
|
98
|
+
| Host-plane install | A row mounted on the host plane (profile layer) has no scope, so its guard lands in the global layer, where narrowing would hit presets that never mounted this plugin. The same probe detects exactly that and the row degrades to guard-only, with one warning. No config key expresses this — the mount shape decides |
|
|
99
|
+
|
|
100
|
+
Membership is deliberately three-state (`member` / `outsider` / `unknown`) and a mask is lifted only on a
|
|
101
|
+
definite `outsider`: an unanswerable probe means "leave that agent alone", because reading it as `outsider` would
|
|
102
|
+
silently undo a working restriction. A probe that cannot answer at all is reported once, loudly.
|
|
75
103
|
|
|
76
|
-
|
|
104
|
+
### Observing the built-ins once masked: four ways
|
|
105
|
+
|
|
106
|
+
| Way | How |
|
|
107
|
+
| --- | --- |
|
|
108
|
+
| **Another composition (another preset)** | Masking is a composition fact, not a global switch: an agent of another composition keeps both tools visible and callable, the control group `tools/probe-mask.mjs` and `tools/repro-mask.mjs` assert; a **sibling agent of the same preset** is masked too, deliberately |
|
|
109
|
+
| **The probe** | `node tools/probe-mask.mjs` mounts the mask on the real dsh packages and registry (see "Self-test and gates"); 15 checks, exit code 2 without an installed dsh |
|
|
110
|
+
| **The built-ins driven directly** | `tools/measure-context.mjs --vs-native` calls `apply()` on the real `dsh-tool-fs` in-process and measures its schemas and sections; no agent is involved, so no mask can reach it |
|
|
111
|
+
| **A temporary lift** | Give the `tool-native-edit-mask` row `disabled: true` |
|
|
112
|
+
|
|
113
|
+
### Mask row configuration
|
|
114
|
+
|
|
115
|
+
The row has no Config schema either: `config:` is passed through as-is.
|
|
116
|
+
|
|
117
|
+
| Key | Default | Meaning |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| `deny` | `['write','edit']` | the names to narrow; only names that agent actually sees are named |
|
|
120
|
+
| `sections` | `['tool:write','tool:edit']` | the guidance sections to shadow with an empty section; `[]` turns the shadowing off |
|
|
121
|
+
|
|
122
|
+
### Boundaries
|
|
123
|
+
|
|
124
|
+
| Boundary | Meaning |
|
|
125
|
+
| --- | --- |
|
|
126
|
+
| **Not an authority boundary** | Visibility composition as DSH defines it: the built-ins **stay registered** and a shell command can still write files |
|
|
127
|
+
| **A composition fact, not a global switch** | The row touches only the agents that joined its composition: the sibling agent of the same preset included, an agent of another preset not at all. A host-plane install is the one exception, and it never narrows |
|
|
128
|
+
| **Names are configuration** | `deny` / `sections` are config, so a rename or split of `tool-fs` is a config edit |
|
|
129
|
+
| **Only visible names are named** | A preset without `tool-fs` gets no restriction at all (never an unknown-name error), and the guard asks visibility per call, leaving a name that exists elsewhere but not for the caller alone |
|
|
77
130
|
|
|
78
131
|
## Tools
|
|
79
132
|
|
|
80
133
|
### `edit_text` — targeted replacement
|
|
81
134
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
`
|
|
135
|
+
| Aspect | Rule |
|
|
136
|
+
| --- | --- |
|
|
137
|
+
| Required | `file_path` and `new_text` |
|
|
138
|
+
| Anchor, exactly one | `old_text` (literal, copied from `read`) / `grep` (regex; the matched line/block including its trailing newline) / `lines` (e.g. `"263:270"`, also including the trailing newline) |
|
|
139
|
+
| `mode` | `replace` (default) / `after` / `before` / `append` / `prepend` |
|
|
140
|
+
| `count` | The expected number of hits: occurrences of the literal for `old_text` (all of them replaced), regex hits for `grep`, covered lines for `lines`. Any mismatch refuses to write |
|
|
141
|
+
| Trailing newline | Both anchor kinds span the line block **with** its trailing newline, so end `new_text` with a newline too; otherwise the replacement joins the following line and the file loses a line (the `+1/-2` stat reports it) |
|
|
142
|
+
| Matching | Exact → relaxed (trailing whitespace, line-block similarity) → nearest candidates on a miss; a match hitting several places without `count` refuses to write, and a relaxed hit adds one `[warn]` line to the result |
|
|
143
|
+
| No k-th hit | `count` is the only disambiguation knob, and it means *confirmation*, not *selection*. To change one occurrence among several, make the anchor unique — a longer `old_text`, or `lines` / `grep` |
|
|
87
144
|
|
|
88
145
|
### `write_text` — create or fully replace a file
|
|
89
146
|
|
|
90
|
-
`file_path` + `content
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
147
|
+
`file_path` + `content`; creation needs no flag (missing parent directories are created) and a brand-new file
|
|
148
|
+
follows the **majority** line-ending style of its siblings (same extension first) with no BOM by default.
|
|
149
|
+
**`content: ''` against a missing target creates a zero-byte file** (stat line `write +0/-0`), while writing
|
|
150
|
+
empty content over an already-empty file is still refused as "no change": a real no-op, not a creation. Both
|
|
151
|
+
tools **write**; neither has a preview mode.
|
|
95
152
|
|
|
96
153
|
### Return value
|
|
97
154
|
|
|
98
|
-
Both tools return the same canonical value (`OUTPUT_SCHEMA`)
|
|
155
|
+
Both tools return the same canonical value (`OUTPUT_SCHEMA`), and it is the whole result: there is no
|
|
156
|
+
presentation channel and no side record. `render` reads `ok`, `brief` and `stderr`; `path` is carried for
|
|
157
|
+
the caller but never rendered.
|
|
99
158
|
|
|
100
159
|
| Field | Content | Destination |
|
|
101
160
|
|---|---|---|
|
|
102
|
-
| `path` | the target path as supplied by the caller
|
|
103
|
-
| `ok`
|
|
104
|
-
| `brief` |
|
|
105
|
-
| `diff` | a unified diff of the changed lines only (`@@` hunk headers, 0 context lines, no `---` / `+++` file headers), limited by `maxDiffLines` | model context |
|
|
106
|
-
| `stdout` | the full human record: path header, complete diff with context lines, backup filename | UI / logs / triage |
|
|
161
|
+
| `path` | the target path as supplied by the caller | canonical value only — **not** rendered |
|
|
162
|
+
| `ok` | whether the write succeeded | model context (`FAIL` vs `WROTE`) |
|
|
163
|
+
| `brief` | one stat line (e.g. `replace@17 +1/-1`) plus any warning lines | model context |
|
|
107
164
|
| `stderr` | failure reason (non-empty on failure) | model context |
|
|
108
165
|
|
|
109
|
-
The model-facing text
|
|
110
|
-
line; the `diff` body carries no `---` / `+++` file headers, so the path never recurs inside it. The
|
|
111
|
-
complete diff is additionally projected by `output.presentationMeta` into a list of
|
|
112
|
-
`{ path, oldText, newText }`, the same card vocabulary the built-in `edit` / `write` tools use, and
|
|
113
|
-
handed to the Web UI by `presentResult`; that metadata is persisted with `tool/result` and never
|
|
114
|
-
enters the model context.
|
|
166
|
+
The model-facing text has exactly two shapes:
|
|
115
167
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
168
|
+
```
|
|
169
|
+
WROTE # success: stat line + warnings
|
|
170
|
+
replace@17 +1/-1
|
|
171
|
+
FAIL # failure: the complete reason (it decides the next call)
|
|
172
|
+
<reason>
|
|
173
|
+
```
|
|
119
174
|
|
|
120
|
-
|
|
175
|
+
**A successful call echoes neither the change nor the path.** Results are appended to the session history, so an
|
|
176
|
+
echo accumulates per call while the caller has just sent `new_text`; `replace@17 +1/-1` already reports which
|
|
177
|
+
lines changed and by how much, and `read` is one call away. The path is bound to its call (`tool/result` carries
|
|
178
|
+
`source.callId`) and the caller's `file_path` is in the same turn's history, so echoing it adds nothing — and it
|
|
179
|
+
costs: across 86 results in 79 real session logs, one success averaged 116 B of which the `WROTE <path>` line was
|
|
180
|
+
55.6 B (48%), against 66 B path-free (−43%). It survives where it carries meaning: in a failure reason that has
|
|
181
|
+
to name the file, and in the GUI's own rendering of the call arguments.
|
|
121
182
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
| `full` | the body is always returned; it is truncated with a one-line note when it exceeds `maxDiffLines` |
|
|
126
|
-
| `none` | no body is returned |
|
|
183
|
+
Failures are reported as an errno plus one reason (`ENOENT`, `ENOTDIR`, `EISDIR`, `EACCES`, …); the absolute
|
|
184
|
+
paths and the internal temp filename (`.<name>.<pid><ts>.tmp`) of the raw Node message stay out of the model
|
|
185
|
+
context.
|
|
127
186
|
|
|
128
|
-
The
|
|
129
|
-
`
|
|
130
|
-
|
|
131
|
-
produces a return of the same order as the content just sent (a measured ~1.0x amplification).
|
|
187
|
+
The one thing the built-ins offer and this plugin does not is a diff body in the result — the built-in `write`
|
|
188
|
+
returns `before` / `after` so the host can draw a card, at 128 B per call. Here the host draws from the call
|
|
189
|
+
arguments it already has.
|
|
132
190
|
|
|
133
191
|
## Deliberate limitations
|
|
134
192
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
- **
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
- **Line anchors are not content-verified.** `lines` and `before` / `after <line>` locate text by line
|
|
144
|
-
number alone: a wrong number does not fail, it edits somewhere else. When the anchor has to be
|
|
145
|
-
verifiable, use `old_text` or `grep`.
|
|
146
|
-
- **Per-target serialization is per process.** An in-process queue per target plus an atomic write
|
|
147
|
-
keeps parallel tool calls from overwriting each other, but another dsh instance, an editor or any
|
|
148
|
-
other process writing the same file still can, and external changes are not detected.
|
|
149
|
-
- **UTF-8 text only.** Files containing NUL bytes (binary) or invalid UTF-8 are refused, as are paths
|
|
150
|
-
inside `.git/` or `.dsh/` and paths outside the workspace.
|
|
193
|
+
| Limitation | Detail |
|
|
194
|
+
| --- | --- |
|
|
195
|
+
| **Writes bypass `ctx.fs`** | The plugin writes the file itself, so the fs-observation policy (read-before-write, version freshness), the sandbox, `sandbox_permissions` escalation and Windows DACL preservation are all skipped; the atomic write is its own (same-directory temp file + fsync + rename). Nothing else enforces anything on this path, so the session file policy is mirrored for the one mode that forbids writing: `read-only` refuses both tools before any I/O, naming the session policy rather than the path. `sandboxPolicy` is consumed opportunistically (`ctx.get`), so a deployment without it, or a resolver that throws, keeps writing instead of bricking |
|
|
196
|
+
| **Only `read-only` is mirrored, and no path is restricted** | Under `workspace-write` and `danger-full-access` both tools write **any** path: outside the workspace, inside `.dsh/` or `.git/`, and through a junction / symlink pointing out of it. The plugin is **not** a security boundary: to restrict where writes land, use the session file policy, the sandbox and `sandbox_permissions` |
|
|
197
|
+
| **Line anchors are not content-verified** | `lines` and `before` / `after <line>` locate text by line number alone: a wrong number does not fail, it edits somewhere else. Where the anchor must be verifiable, use `old_text` or `grep` |
|
|
198
|
+
| **Per-target serialization is per process** | An in-process queue per target plus an atomic write keeps parallel tool calls from overwriting each other, but another dsh instance, an editor or any other process writing the same file still can, and external changes are not detected |
|
|
199
|
+
| **UTF-8 text only** | Files containing NUL bytes (binary) or invalid UTF-8 are refused; a file marked read-only by the OS is refused too (the atomic rename fails with `EPERM`) |
|
|
200
|
+
| **Creating a file fills in missing parent directories** | A missing `write_text` target gets its parents created (`mkdir -p`, as the built-in `write` does), with no extra output |
|
|
151
201
|
|
|
152
202
|
## Configuration
|
|
153
203
|
|
|
@@ -155,61 +205,85 @@ There is no Config schema: the preset row's `config:` mapping is passed through
|
|
|
155
205
|
|
|
156
206
|
| Key | Default | Meaning |
|
|
157
207
|
|---|---|---|
|
|
158
|
-
| `backup` | `true` | copy the previous content into `artifactsDir/backups` before writing |
|
|
159
|
-
| `ledger` | `true` | append a JSONL record to `artifactsDir/edits.log` |
|
|
160
|
-
| `artifactsDir` | `<workspace>/.dsh` | where backups and the ledger live |
|
|
161
208
|
| `newFileBom` | `false` | write a UTF-8 BOM when creating a new file |
|
|
162
|
-
| `
|
|
163
|
-
| `diff` | `'auto'` | default policy for the `diff` field (`auto` / `full` / `none`); a per-call `diff` argument takes precedence |
|
|
164
|
-
| `maxDiffLines` | `30` | line limit for the `diff` field: `auto` omits the body when exceeded, `full` truncates at it |
|
|
209
|
+
| `guidance` | `'full'` | three modes: `full` (names the built-ins) / `short` (for when the mask hides them) / `false` (register no section) |
|
|
165
210
|
| `root` | `process.cwd()` | fallback workspace when a call has no agent session |
|
|
166
211
|
|
|
167
212
|
`DSH_TEXT_EDITOR_EOL` (`lf` \| `crlf`) overrides the line-ending inference for **new** files.
|
|
168
213
|
|
|
214
|
+
## Versus the built-ins
|
|
215
|
+
|
|
216
|
+
Measured against a real installation (`dsh 0.1.5-rc.2`, `lib/editor.mjs` vs `dsh-tool-fs`), each row
|
|
217
|
+
writing the same fixture twice:
|
|
218
|
+
|
|
219
|
+
| Fixture | Built-in | This plugin |
|
|
220
|
+
|---|---|---|
|
|
221
|
+
| CRLF file, `write` with CRLF content | 3 CRLF in, 3 out (content is written verbatim) | same |
|
|
222
|
+
| CRLF file, `write` with LF content | **3 CRLF in, 0 out** — the file silently becomes LF | CRLF kept: the file's own style wins |
|
|
223
|
+
| BOM + CRLF file, either tool | **BOM gone** | BOM kept |
|
|
224
|
+
| Anchor missing a trailing space | `FS_EDIT_NOT_FOUND`, no fallback (a re-read is the only recovery) | relaxed line-block match, one `[warn]` line |
|
|
225
|
+
| Literal appearing twice | refused (`FS_AMBIGUOUS_EDIT`) unless `replace_all: true` | refused unless `count` declares it |
|
|
226
|
+
| Success result text | 128 B, echoing `before` / `after` for the GUI | 17–21 B, echoing nothing |
|
|
227
|
+
|
|
228
|
+
So the honest accounting is: **one correctness fix (the BOM) and one behaviour fix (line-ending style), plus
|
|
229
|
+
cheaper results.** The rest is convenience — anchors that name a position, and a fallback for near-miss anchors.
|
|
230
|
+
If you never touch BOM files, the only thing you gain is tokens, and you pay for it with two competing write
|
|
231
|
+
tools in the model's catalog.
|
|
232
|
+
|
|
169
233
|
## Self-test and gates
|
|
170
234
|
|
|
171
235
|
```powershell
|
|
172
236
|
# run from the root of a clone of this repository
|
|
173
|
-
node tools/selftest.mjs
|
|
174
|
-
node tools/check-license.mjs
|
|
175
|
-
node tools/gen-schema.mjs
|
|
237
|
+
node tools/selftest.mjs # 148/148 on Windows + Node 24
|
|
238
|
+
node tools/check-license.mjs # 30/30 license / dependency / Node-only gate
|
|
239
|
+
node tools/gen-schema.mjs # embedded schemas still match the DSL
|
|
240
|
+
node tools/measure-context.mjs # per-scenario model-visible bytes
|
|
241
|
+
node tools/audit-session.mjs # reconcile against real session logs (text-shape check)
|
|
242
|
+
node tools/audit-session.mjs --tools # the real tool table of every request/header (mask acceptance)
|
|
243
|
+
node tools/probe-mask.mjs # the mask on real dsh packages: registry semantics (15 checks, exit 2 without them)
|
|
244
|
+
node tools/repro-mask.mjs # the mask on real presets + agents: composition timing (23 checks, exit 2 without them)
|
|
176
245
|
```
|
|
177
246
|
|
|
178
|
-
|
|
179
|
-
|
|
247
|
+
`npm test` chains the licence gate, the self-test and `measure-context --cap 2048`: no single call may put
|
|
248
|
+
more than 2 KB of model-visible text into the context. The current worst scenario is the 1.6 KB ambiguity
|
|
249
|
+
hint; a successful call is always two lines, 17–21 B.
|
|
180
250
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
plugin-layer suite** that drives `apply()` with a fake context and asserts tool registration, the
|
|
185
|
-
guidance section, that every returned value satisfies `OUTPUT_SCHEMA`, the `render()` text, and the
|
|
186
|
-
config plumbing (`root` / `backup` / `ledger` / `newFileBom`) — **and a return-value suite**: a
|
|
187
|
-
full-file rewrite must not echo the content back, a small edit must still report the changed lines,
|
|
188
|
-
the three `diff` values must hold their documented boundaries, the path must appear once, and the
|
|
189
|
-
complete diff must be projected only through `presentationMeta`.
|
|
251
|
+
These live in the repository only: `tools/` is deliberately outside the `files` whitelist, so the published
|
|
252
|
+
package is just the plugin, its preset installer, the docs and the license. Each script's own header documents
|
|
253
|
+
what it asserts and why; what follows is the shape of each.
|
|
190
254
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
255
|
+
| Tool | Behaviour |
|
|
256
|
+
| --- | --- |
|
|
257
|
+
| `tools/selftest.mjs` | The end-to-end suite, needing no dsh: BOM/EOL fidelity, all four anchor kinds, `count`, ambiguity refusal, relaxed matching (the similarity threshold pinned on both sides), usage errors, binary/invalid-UTF-8 refusal, majority EOL inference, concurrent writes, parent-directory creation, errno-only failure text — plus a plugin layer (`apply()` on a fake context: registration, the guidance section, every value satisfying `OUTPUT_SCHEMA`, the literal `render()` text, config plumbing, no `.dsh/` left behind), a return-value layer, a policy layer (`read-only` refusals), a mask layer (a fake world for the guard, the membership probe, the sweep, disposal and the unload hook) and a guidance layer. 148 checks |
|
|
258
|
+
| `tools/probe-mask.mjs` | Mask registry semantics on the real packages and the real `dsh-agent` registry, mounted on a real scoped context: a masked agent loses `write` / `edit` and naming one yields `UNKNOWN_TOOL`, another composition keeps both, the standing scope still has them registered, an agent with no creation event is still blocked by the apply-time guard, and a host-plane row degrades to guard-only. 15 checks; exit 2 without a dsh installation |
|
|
259
|
+
| `tools/repro-mask.mjs` | Mask composition timing on the real `dsh-agent-presets` service: created after the mount, re-linked after creation, first bind, switched away, and a child agent via `composeFrom`, with a foreign-guard host and another composition as controls. 23 checks; the regression test for a timing bug that shipped once (4/7 before the fix), exit 2 without the packages |
|
|
260
|
+
|
|
261
|
+
### Measuring context cost
|
|
262
|
+
|
|
263
|
+
| Tool | Behaviour |
|
|
264
|
+
| --- | --- |
|
|
265
|
+
| `tools/measure-context.mjs` | Drives `apply()` on a simulated context through the real `execute()` → `render()` path, printing input bytes, model-visible bytes, ratio and line count per scenario. `--cap N` exits 1 when a scenario exceeds N bytes; `--static` prints the per-request overhead; `--vs-native` adds the same figures for the host's `write` / `edit` (SKIP without a dsh installation). `--static --vs-native` is the source of the masking figures above |
|
|
266
|
+
| `tools/audit-session.mjs` | Reconciles real session logs (`<DSH_HOME>/sessions/`, multi-frame zstd) per call: whether each result matches one of the two documented shapes (`WROTE` + a stat line, or `FAIL` + a reason), whether a success repeats the call's own `file_path`, and whether a result exceeds `--cap` (1024 B by default). Results from before this shape landed are reported separately as legacy. `--tools` switches to the tool-table view described above |
|
|
267
|
+
| `tools/gen-schema.mjs` | Checks that the embedded schemas still match the author DSL in the same file. Needs an installed `@deepseek-ai/dsh-tools` (`DSH_TOOLS_ENTRY` overrides the lookup); exit 2 without one |
|
|
194
268
|
|
|
195
269
|
## Layout
|
|
196
270
|
|
|
197
271
|
```
|
|
198
|
-
lib/core.mjs #
|
|
199
|
-
lib/editor.mjs #
|
|
272
|
+
lib/core.mjs # core: BOM/EOL, anchors, matching, atomic write, per-target lock
|
|
273
|
+
lib/editor.mjs # plugin: schemas, validation, registration, read-only mirror (zero-dep ESM)
|
|
274
|
+
lib/mask.mjs # optional row: mask the built-in write / edit per agent (guard + restrict)
|
|
200
275
|
preset/preset.yml # preset name/description, as dsh lists it
|
|
201
|
-
scripts/install-preset.mjs # derives the user preset from the local dsh installation
|
|
276
|
+
scripts/install-preset.mjs # derives the user preset from the local dsh installation (--mask-native adds the mask)
|
|
202
277
|
cordis.patch.yml # host-plane bundle patch
|
|
203
|
-
tools/selftest.mjs # end-to-end self-test (core + plugin
|
|
278
|
+
tools/selftest.mjs # end-to-end self-test (core + plugin + result text + policy + mask + guidance)
|
|
279
|
+
tools/probe-mask.mjs # mask registry semantics on real dsh packages + registry (exit 2 without them)
|
|
280
|
+
tools/repro-mask.mjs # mask composition timing on real presets + agents (exit 2 without them)
|
|
204
281
|
tools/check-license.mjs # license / dependency / Node-only hygiene gate
|
|
205
282
|
tools/gen-schema.mjs # authoritative source and checker for the embedded JSON Schemas
|
|
283
|
+
tools/measure-context.mjs # per-scenario model-visible bytes + the native-tool comparison
|
|
284
|
+
tools/audit-session.mjs # real session logs: text-shape reconciliation + the --tools tool-table view
|
|
206
285
|
```
|
|
207
286
|
|
|
208
|
-
Backups and the ledger use fixed, documented names and fields: one file per edit under
|
|
209
|
-
`.dsh/backups/`, named `<flattened-absolute-path>@<timestamp>`, and one JSON object per line in
|
|
210
|
-
`.dsh/edits.log` (`time`, `id`, `tool`, `file`, `abspath`, `action`, `kinds`, `line_start`,
|
|
211
|
-
`line_end`, `added`, `removed`, `bom`, `eol`, `backup`, `summary`).
|
|
212
|
-
|
|
213
287
|
## License
|
|
214
288
|
|
|
215
289
|
**Apache-2.0** — see [LICENSE](LICENSE). Copyright 2026 Flotiarenor. The package has **no runtime
|
|
@@ -217,6 +291,6 @@ dependencies**, so it carries no third-party license obligations of its own. Thr
|
|
|
217
291
|
|
|
218
292
|
- The embedded JSON Schemas in `lib/editor.mjs` are generated *output* of the `@deepseek-ai/dsh-tools`
|
|
219
293
|
converter (MIT, Copyright (c) 2026 DeepSeek) via `tools/gen-schema.mjs`.
|
|
220
|
-
- The preset composition is **not** part of this package: `scripts/install-preset.mjs` reads the one
|
|
221
|
-
|
|
294
|
+
- The preset composition is **not** part of this package: `scripts/install-preset.mjs` reads the one shipped
|
|
295
|
+
with the user's own dsh installation at install time.
|
|
222
296
|
- Source files carry an `SPDX-License-Identifier` header, so the license is machine-readable per file.
|