@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.
@@ -2,22 +2,15 @@
2
2
  // SPDX-FileCopyrightText: 2026 Flotiarenor
3
3
  // SPDX-License-Identifier: Apache-2.0
4
4
  /**
5
- * install-preset.mjs —— 把 `edit_text` / `write_text` 装成一个**用户 preset**。
5
+ * install-preset.mjs —— 把 `edit_text` / `write_text` 装成一个用户 preset:读本机 dsh 自带的 preset 组合
6
+ * (默认 `standard`),插入 `tool-text-editor` 行,连同 `preset/preset.yml` 写到
7
+ * `<DSH_HOME>/.agent-presets/<id>/`。
6
8
  *
7
- * 做法:读**本机 dsh 自带的 preset 组合**(默认 `standard`),把 `tool-text-editor` 那一行插进去,
8
- * 再连同本仓库的 `preset/preset.yml` 写到 `<DSH_HOME>/.agent-presets/<id>/`。
9
+ * 组合必须从用户自己的 dsh 派生,不在仓库里放拷贝:自带组合是别人(MIT, Copyright (c) 2026 DeepSeek)的
10
+ * 作品,随包分发要连带履行其署名义务。
9
11
  *
10
- * 为什么不在仓库里放一份 preset 组合的拷贝:
11
- * * dsh 自带的组合是**别人(MIT, Copyright (c) 2026 DeepSeek)的作品**,随包分发它就要连带履行
12
- * 它的署名义务,而这份拷贝与本插件的功能无关;
13
- * * 从用户自己的 dsh 里取,preset 自然跟着他装的 dsh 版本走 —— 不会像拷贝那样随 dsh 升级而过期。
14
- *
15
- * 用法:
16
- * node scripts/install-preset.mjs # 默认 --id texteditor --base standard
17
- * node scripts/install-preset.mjs --id my-edit --base code
18
- * node scripts/install-preset.mjs --from <path-to-agent.cordis.yml> # 自己指定源组合
19
- * node scripts/install-preset.mjs --force # 覆盖已存在的 preset(只覆盖两个文件)
20
- * node scripts/install-preset.mjs --dry-run # 只打印会做什么,不落盘
12
+ * 用法:`node scripts/install-preset.mjs [--id <id>] [--base <id>] [--from <组合路径>] [--force] [--dry-run]
13
+ * [--mask-native]`;`--id` 默认 `texteditor`,`--base` 默认 `standard`。
21
14
  *
22
15
  * 退出码:0 成功,1 失败,2 用法错误 / 找不到 dsh 自带的 preset 组合。
23
16
  */
@@ -30,8 +23,17 @@ import { fileURLToPath } from 'node:url'
30
23
  const HERE = dirname(fileURLToPath(import.meta.url))
31
24
  const REPO = resolve(HERE, '..')
32
25
  const PLUGIN = join(REPO, 'lib', 'editor.mjs').replace(/\\/g, '/')
26
+ const MASK = join(REPO, 'lib', 'mask.mjs').replace(/\\/g, '/')
33
27
  const META = join(REPO, 'preset', 'preset.yml')
34
- const SHIPPED_PRESET_DIR = ['config', 'agent-presets']
28
+ /**
29
+ * 自带 preset 组合的两种布局(按版本从上到下试):`@deepseek-ai/dsh/config/agent-presets/<base>/`
30
+ * (≤ 0.1.0-rc.6)与 `@deepseek-ai/dsh-agent-presets/presets/<base>/`(0.1.5-rc.2 起自带组合搬进另一个
31
+ * 包,该包按 `new URL('../presets/', import.meta.url)` 定位,旧路径在新版里已不存在)。
32
+ */
33
+ const SHIPPED_LAYOUTS = [
34
+ ['dsh', 'config', 'agent-presets'],
35
+ ['dsh-agent-presets', 'presets'],
36
+ ]
35
37
  const COMPOSITION = 'agent.cordis.yml'
36
38
 
37
39
  function flagValue(name) {
@@ -39,30 +41,29 @@ function flagValue(name) {
39
41
  if (index < 0) return undefined
40
42
  const value = process.argv[index + 1]
41
43
  if (value === undefined || value.startsWith('--')) {
42
- console.error('FAIL ' + name + ' 缺少取值')
44
+ console.error('FAIL ' + name + ' needs a value')
43
45
  process.exit(2)
44
46
  }
45
47
  return value
46
48
  }
47
49
 
48
50
  /**
49
- * dsh 可能装在任何位置,所以按布局枚举"自带 preset 组合"的候选路径(不写死本机路径):
50
- * 1. `--from` / `DSH_PRESET_SOURCE` —— 显式指定,任何布局都能用;
51
- * 2. dsh profile 的 node_modules(`<DSH_HOME|~/.dsh>/profiles/node_modules`);
52
- * 3. npm 全局前缀下的 node_modules(Windows `%APPDATA%\npm`;POSIX `/usr/local/lib`、
53
- * `/usr/lib`、`~/.npm-global/lib`)。
54
- * @param base - 自带 preset 的 id(standard / code / cordis / minimal)。
55
- * @returns 候选绝对路径(按优先级)。
51
+ * 枚举自带 preset 组合的候选路径(dsh 可能装在任何位置,故不写死本机路径):`--from` /
52
+ * `DSH_PRESET_SOURCE` → profile 的 node_modules(`<DSH_HOME>` 与默认 `~/.dsh` 都试:`DSH_HOME` 可能被指到
53
+ * 别处如临时目录,而 dsh 本体仍在默认 home 下)→ npm 全局前缀。
54
+ * @param explicitPath - 显式指定的源组合(已确认存在)。
56
55
  */
57
- function findCompositions(base) {
58
- const candidates = []
59
- const explicit = flagValue('--from') ?? process.env.DSH_PRESET_SOURCE
60
- if (typeof explicit === 'string' && explicit !== '') candidates.push(resolve(explicit))
56
+ function findCompositions(base, explicitPath, dshHome) {
57
+ const candidates = explicitPath === undefined ? [] : [explicitPath]
61
58
  const add = (nodeModules) => {
62
- if (typeof nodeModules !== 'string' || nodeModules === '') return
63
- candidates.push(join(nodeModules, '@deepseek-ai', 'dsh', ...SHIPPED_PRESET_DIR, base, COMPOSITION))
59
+ if (nodeModules === '') return
60
+ for (const layout of SHIPPED_LAYOUTS) {
61
+ candidates.push(join(nodeModules, '@deepseek-ai', ...layout, base, COMPOSITION))
62
+ }
63
+ }
64
+ for (const home of new Set([dshHome, join(homedir(), '.dsh')])) {
65
+ add(join(home, 'profiles', 'node_modules'))
64
66
  }
65
- add(join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'profiles', 'node_modules'))
66
67
  const globalRoots = process.platform === 'win32'
67
68
  ? [process.env.APPDATA === undefined ? '' : join(process.env.APPDATA, 'npm', 'node_modules')]
68
69
  : ['/usr/local/lib/node_modules', '/usr/lib/node_modules', join(homedir(), '.npm-global', 'lib', 'node_modules')]
@@ -70,40 +71,64 @@ function findCompositions(base) {
70
71
  return candidates
71
72
  }
72
73
 
73
- /** 我们插进组合里的那一段(只有这一段是我们自己的文字 + 行)。 */
74
- function pluginBlock(sourcePath) {
75
- return [
74
+ /**
75
+ * 插进组合里的那一段(只有这一段是本仓库自己的文字与行)。
76
+ * @param sourcePath - 源组合路径(写进注释,便于升级后重跑)。
77
+ * @param maskNative - 是否插入屏蔽原生 write/edit 的门禁行,并让编辑行改用短引导。
78
+ * @returns 插件段文本(以单个换行结尾)。
79
+ */
80
+ function pluginBlock(sourcePath, maskNative) {
81
+ const lines = [
76
82
  '# ── 字节保真的文本编辑工具(edit_text / write_text)─────────────────────────',
77
83
  '#',
78
- '# 存在理由:原生 `write` 会丢掉 UTF-8 BOM 并把 CRLF 文件改写成 LF,原生 `edit` 也会丢掉 BOM;',
79
- '# 这两个工具把 BOM 与行尾都保住,另外带上 dry-run diff、落盘前备份、编辑台账、grep/lines 锚点',
80
- '# 与"匹配失败给最接近候选"。实现是**进程内 Node**:零依赖、零外部运行时、每次调用没有进程',
81
- '# 启动开销(不启动任何解释器或外部命令)。',
84
+ '# 原生 `edit` / `write` 都会丢 UTF-8 BOM,且不还原文件自身的行尾(往 CRLF 文件写 LF 内容就变成 LF);',
85
+ '# 原生 `edit` 还只做精确匹配(`old_string` 差一个空格就报 FS_EDIT_NOT_FOUND)。本行两个工具保 BOM 与文件',
86
+ '# 自身行尾,精确失败时按行块相似度回退并给出最接近的候选。read-only 会话下在任何 I/O 之前拒写;模型可见',
87
+ '# 文本只有一行统计。',
82
88
  '#',
83
- '# 原生 `edit`/`write` **保留不动**:本行注册的是两个**不同名**工具,同一层不会同名冲突,',
84
- '# 想回退只需给这一行加 `disabled: true`(或整行删掉)。',
89
+ ...(maskNative
90
+ ? ['# 原生 `edit`/`write` 由下面的门禁行按 agent 作用域屏蔽,本行因此改用 `guidance: short`。']
91
+ : ['# 原生 `edit`/`write` 保留不动:注册的是两个不同名工具,同一层不会同名冲突(回退:给本行加',
92
+ '# `disabled: true`,或整行删掉)。']),
85
93
  '#',
86
- '# 本文件由 `scripts/install-preset.mjs` 生成:源 = 本机 dsh 自带的 preset 组合',
94
+ '# 由 `scripts/install-preset.mjs` 从本机 dsh 自带的 preset 组合派生:',
87
95
  `# ${sourcePath}`,
88
- '# 行名写的是本仓库 `lib/editor.mjs` 的绝对路径 —— preset 行的**裸包名**会从宿主组装基址解析,',
89
- '# 但模块**内部的**裸 import 由 Node 按文件真实路径解析,而 preset 目录下没有 node_modules,',
90
- '# 所以该插件刻意零依赖(只用 node: 内置模块),可以放在任何位置。',
91
- '#',
92
- '# 该插件消费宿主服务(tools / systemPrompt),不发布任何服务,因此不需要 isolate realm。',
96
+ '# 行名写绝对路径:preset 行的裸包名由宿主组装基址解析,模块内部的裸 import 由 Node 按文件真实路径解析,',
97
+ '# 而 preset 目录下没有 node_modules —— 该插件刻意零依赖(只用 node: 内置模块),可放任何位置。',
93
98
  '#',
94
99
  '# 可选 config(插件没有 Config schema,字段原样透传):',
95
- '# backup / ledger: boolean 默认都 true(备份到 artifactsDir/backups,台账 artifactsDir/edits.log)',
96
- '# artifactsDir: <路径> 默认 <会话工作区>/.dsh',
97
100
  '# newFileBom: boolean 默认 false(新建文件是否写 BOM)',
98
- '# context: number diff 上下文行数,默认 3',
99
101
  '# root: <路径> 没有 agent 会话时的回退工作区',
102
+ '# guidance: full|short|false 默认 full;short 去掉"优先于原生"那半句',
100
103
  '- id: tool-text-editor',
101
104
  ` name: '${PLUGIN}'`,
102
- '',
103
- ].join('\n')
105
+ ...(maskNative ? [' config:', ' guidance: short'] : []),
106
+ ]
107
+ if (maskNative) {
108
+ lines.push(
109
+ '',
110
+ '# ── 屏蔽原生的 write / edit(每个 agent 的作用域)──────────────────────────',
111
+ '#',
112
+ '# 原生两个工具仍在工具表里:每次请求付 1754 B 的两个 schema 加 608 B 的两段引导,只为劝模型别用它们。',
113
+ '# 本行按 agent 作用域收窄,三条通路任一条先到就先收窄(早先只挂 `agent/created`,而 GUI 是"先建 agent、',
114
+ '# 后换 preset",重挂是父级 re-link,那个事件早已发完):',
115
+ '# * `apply` 阶段就挂守卫:与创建顺序无关,第一次直呼原生工具即被否决并顺手收窄;',
116
+ '# * `agent/created`:建档时收窄本 preset 的 agent(含 subagent);',
117
+ '# * `tools/change`:换 preset 时 `recompose()` 发它,此时枚举活 agent 收窄属于本组合的,反向换出的成对撤销。',
118
+ '#',
119
+ '# 收窄用 `agent.ctx.tools.restrict({ deny })`:注册表只有一套可见性解析器,被拒的名字既不在工具表里也调不动',
120
+ '# (直呼得到 UNKNOWN_TOOL);另注册同名空段遮蔽 tool:write / tool:edit 引导。只影响选了本 preset 的会话,',
121
+ '# 其它 preset 照旧可用(天然对照组);只点名本 agent 真的看得见的工具,归属判据靠探测对象的身份。',
122
+ '# 回退:删掉本行,或给编辑行加 `guidance: full`。',
123
+ '- id: tool-native-edit-mask',
124
+ ` name: '${MASK}'`,
125
+ )
126
+ }
127
+ lines.push('')
128
+ return lines.join('\n')
104
129
  }
105
130
 
106
- /** 插入位置:dsh 自带组合里"文件系统"之后、"后台任务"之前;找不到锚点就追加到末尾。 */
131
+ /** 插入位置:自带组合里"文件系统"之后、"后台任务"之前;找不到锚点就追加到末尾。 */
107
132
  const ANCHORS = [
108
133
  { pattern: /^# ── background jobs/m, label: 'background jobs 段之前' },
109
134
  { pattern: /^- id: tool-jobs$/m, label: 'tool-jobs 行之前' },
@@ -114,11 +139,11 @@ const ANCHORS = [
114
139
  * @returns `{ text, anchor }`
115
140
  * @throws {Error} 源组合看起来已经打过补丁时。
116
141
  */
117
- function inject(source, sourcePath) {
142
+ function inject(source, sourcePath, maskNative) {
118
143
  if (/^- id: tool-text-editor$/m.test(source)) {
119
- throw new Error('源组合里已经有 tool-text-editor 行了 —— 请指向 dsh 自带的原始组合')
144
+ throw new Error('the source composition already has a tool-text-editor row -- point at the unmodified composition shipped with dsh')
120
145
  }
121
- const block = pluginBlock(sourcePath)
146
+ const block = pluginBlock(sourcePath, maskNative)
122
147
  for (const { pattern, label } of ANCHORS) {
123
148
  const match = pattern.exec(source)
124
149
  if (match !== null) {
@@ -130,59 +155,82 @@ function inject(source, sourcePath) {
130
155
  return { text: source + separator + block, anchor: '文件末尾' }
131
156
  }
132
157
 
133
- // ── 参数与前置检查 ──────────────────────────────────────────────────────────
158
+ // ── 参数与前置检查 ──
134
159
 
135
160
  const id = flagValue('--id') ?? 'texteditor'
136
161
  const base = flagValue('--base') ?? 'standard'
137
162
  const force = process.argv.includes('--force')
138
163
  const dryRun = process.argv.includes('--dry-run')
139
- const fromFlag = flagValue('--from')
164
+ const maskNative = process.argv.includes('--mask-native')
165
+ /** `--from` / `DSH_PRESET_SOURCE`:显式源组合,给定时不再枚举自带布局。 */
166
+ const explicitSource = (flagValue('--from') ?? process.env.DSH_PRESET_SOURCE ?? '').trim()
167
+ const explicitPath = explicitSource === '' ? undefined : resolve(explicitSource)
168
+ /** 生效的用户目录:`DSH_HOME` 非空则用它,否则 `~/.dsh`。 */
169
+ const dshHome = process.env.DSH_HOME?.trim() || join(homedir(), '.dsh')
140
170
 
141
171
  if (!/^[a-z0-9][a-z0-9-]*$/.test(id)) {
142
- console.error('FAIL preset id 必须是 [a-z0-9][a-z0-9-]*(会作为目录名),收到:' + id)
172
+ console.error('FAIL the preset id must match [a-z0-9][a-z0-9-]* (it becomes a directory name), got: ' + id)
143
173
  process.exit(2)
144
174
  }
145
175
  if (!/^[a-z0-9][a-z0-9-]*$/.test(base)) {
146
- console.error('FAIL --base 必须是 dsh 自带 preset 的 id(如 standard / code / minimal),收到:' + base)
176
+ console.error('FAIL --base must be the id of a preset shipped with dsh (standard / minimal / cordis / ptc), got: ' + base)
147
177
  process.exit(2)
148
178
  }
149
179
  if (!existsSync(PLUGIN)) {
150
- console.error('FAIL 找不到插件文件:' + PLUGIN)
180
+ console.error('FAIL plugin file not found: ' + PLUGIN)
181
+ process.exit(1)
182
+ }
183
+ if (maskNative && !existsSync(MASK)) {
184
+ console.error('FAIL mask file not found: ' + MASK + ' (--mask-native needs it)')
151
185
  process.exit(1)
152
186
  }
153
187
  if (!existsSync(META)) {
154
- console.error('FAIL 找不到 preset 元数据:' + META)
188
+ console.error('FAIL preset metadata not found: ' + META)
155
189
  process.exit(1)
156
190
  }
191
+ if (explicitPath !== undefined && !existsSync(explicitPath)) {
192
+ console.error('FAIL --from / DSH_PRESET_SOURCE points at a composition that does not exist: ' + explicitPath)
193
+ process.exit(2)
194
+ }
157
195
 
158
- const candidates = findCompositions(base)
196
+ const candidates = findCompositions(base, explicitPath, dshHome)
159
197
  const sourcePath = candidates.find((candidate) => existsSync(candidate))
160
198
  if (sourcePath === undefined) {
161
- console.error(`FAIL 找不到本机 dsh 自带的 preset 组合(--base ${base});试过:`)
199
+ console.error(`FAIL no preset composition shipped with the local dsh (--base ${base}); tried:`)
162
200
  for (const candidate of candidates) console.error(' ' + candidate)
163
- console.error(' 装了 dsh 就有;也可以用 --from <agent.cordis.yml 路径> 或 DSH_PRESET_SOURCE 指定。')
201
+ console.error(' installing dsh provides it; you can also point --from <agent.cordis.yml path> or DSH_PRESET_SOURCE at one.')
164
202
  process.exit(2)
165
203
  }
166
204
 
167
205
  const source = readFileSync(sourcePath, 'utf8')
168
206
  let injected
169
207
  try {
170
- injected = inject(source, sourcePath)
208
+ injected = inject(source, sourcePath, maskNative)
171
209
  } catch (error) {
172
210
  console.error('FAIL ' + error.message)
173
211
  process.exit(2)
174
212
  }
175
213
 
176
- const dshHome = process.env.DSH_HOME && process.env.DSH_HOME.trim() !== ''
177
- ? process.env.DSH_HOME.trim()
178
- : join(homedir(), '.dsh')
214
+ /**
215
+ * 写出的 `preset.yml` 必须与实际组合一致:仓库那份与模式无关,而"原生工具是否被屏蔽"随 `--mask-native` 变
216
+ * (旧版原样拷贝,开着门禁的描述里也写着"原生 edit/write 保持不变")。状态句接在 `preset/preset.yml` 的最后
217
+ * 一行 `description:` 之后;它是 YAML 纯标量,不能出现 `: ` 或 ` #`。
218
+ */
219
+ function metadataText() {
220
+ const state = maskNative
221
+ ? ';原生 `edit`/`write` 已被门禁行按 agent 作用域屏蔽:既不出现在工具表里,也调不动。'
222
+ : ';原生 edit/write 保持不变。'
223
+ return readFileSync(META, 'utf8').trimEnd() + state + '\n'
224
+ }
225
+
179
226
  const targetDir = join(dshHome, '.agent-presets', id)
180
227
  const targetComposition = join(targetDir, COMPOSITION)
181
228
  const targetMeta = join(targetDir, 'preset.yml')
182
229
 
183
230
  console.log('仓库 : ' + REPO)
184
231
  console.log('插件 : ' + PLUGIN)
185
- console.log('源组合 : ' + sourcePath + (fromFlag === undefined && process.env.DSH_PRESET_SOURCE === undefined ? `(--base ${base})` : ''))
232
+ if (maskNative) console.log('门禁 : ' + MASK + '(屏蔽原生 write/edit,编辑行 guidance: short)')
233
+ console.log('源组合 : ' + sourcePath + (explicitPath === undefined ? `(--base ${base})` : ''))
186
234
  console.log('插入位置 : ' + injected.anchor)
187
235
  console.log('DSH_HOME : ' + dshHome)
188
236
  console.log('目标 preset : ' + targetDir)
@@ -195,18 +243,18 @@ if (dryRun) {
195
243
  console.log(' ' + targetComposition + `(源组合 ${source.split('\n').length} 行 + 插件段)`)
196
244
  console.log(' ' + targetMeta)
197
245
  if (exists && !force) {
246
+ // 只预告冲突:dry-run 不落盘,退出码按契约仍为 0。
198
247
  console.error('')
199
- console.error('[dry-run] 但目标已存在,真跑会被拒绝:' + targetComposition)
200
- console.error(' 要覆盖请加 --force(只覆盖 agent.cordis.yml 与 preset.yml,同目录其它文件不动)。')
201
- process.exit(1)
248
+ console.error('[dry-run] but the target already exists, so a real run would be refused: ' + targetComposition)
249
+ console.error(' add --force to overwrite (only agent.cordis.yml and preset.yml are replaced; other files in that directory are left alone).')
202
250
  }
203
251
  process.exit(0)
204
252
  }
205
253
 
206
254
  if (exists && !force) {
207
255
  console.error('')
208
- console.error('FAIL 该 preset 已存在:' + targetComposition)
209
- console.error(' 要覆盖请加 --force(只覆盖 agent.cordis.yml 与 preset.yml,同目录其它文件不动)。')
256
+ console.error('FAIL that preset already exists: ' + targetComposition)
257
+ console.error(' add --force to overwrite (only agent.cordis.yml and preset.yml are replaced; other files in that directory are left alone).')
210
258
  process.exit(1)
211
259
  }
212
260
  if (exists) {
@@ -215,7 +263,7 @@ if (exists) {
215
263
 
216
264
  mkdirSync(targetDir, { recursive: true })
217
265
  writeFileSync(targetComposition, injected.text, 'utf8')
218
- writeFileSync(targetMeta, readFileSync(META, 'utf8'), 'utf8')
266
+ writeFileSync(targetMeta, metadataText(), 'utf8')
219
267
 
220
268
  console.log('')
221
269
  console.log('OK 已安装 preset "' + id + '"')
@@ -223,5 +271,10 @@ console.log('下一步:')
223
271
  console.log(' 1. 重启 dsh web(preset 名单在启动时读取;运行中的会话不会换 preset)')
224
272
  console.log(' 2. 新建一个会话,preset 选 "' + id + '"')
225
273
  console.log(' 3. 会话里直接用 edit_text / write_text(纯 Node 进程内实现:零依赖、零外部运行时)')
274
+ if (maskNative) {
275
+ console.log(' 该 preset 的会话里原生 write/edit 既不出现在工具表里、也调不动(直呼得到 UNKNOWN_TOOL);')
276
+ console.log(' 想对比或观察原生行为,用别的 preset(如 standard)新建会话即可 —— 门禁只作用于本 preset。')
277
+ }
226
278
  console.log('升级 dsh 后重跑本脚本(加 --force)即可让 preset 跟上新版自带组合。')
227
279
  console.log('回退:给 ' + targetComposition + ' 里的 tool-text-editor 行加 disabled: true,或删掉 ' + targetDir)
280
+ if (maskNative) console.log(' 只想撤掉屏蔽:删掉同文件里的 tool-native-edit-mask 行,并把编辑行的 guidance 改回 full。')