dsh-bare-preset 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +36 -2
- package/cordis.patch.yml +3 -0
- package/lib/index.js +231 -0
- package/package.json +40 -4
- package/persona.mjs +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-bare-preset contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-bare-preset
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
「裸模模式」agent 预设,做成一个**插件**(自己向 `agentPresets` 注册),顺手把宿主强加给 agent 的运行时开销一并剪掉。
|
|
4
|
+
|
|
5
|
+
## 为什么不用预设目录 + bundle patch
|
|
6
|
+
|
|
7
|
+
那些坑都踩过:bundle patch 里的行要注册进 registry,而 registry 用它**自己的** baseUrl 解析相对路径(`./persona-file.mjs` 直接 import 失败,预设从选择器里消失);`id` 必须是字符串;声明归属还要和「预设工作台」的运行时声明抢所有权。
|
|
8
|
+
|
|
9
|
+
现在这个包自己做主:激活时 `presets.register()`,卸载时注销。组合里的提示词行用包内绝对 file URL,没有任何路径歧义。
|
|
10
|
+
|
|
11
|
+
## 剪掉的三层
|
|
12
|
+
|
|
13
|
+
| 层 | 谁加的 | 手段 | 省 |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| 系统提示词 | — | 人设行 `complete: true` + 空文本,`suppressRuntimeContext()` | 全部 |
|
|
16
|
+
| 全局工具 | 宿主各插件 | `tools.restrict({ allow: [] })`(persona 行) | 全部 |
|
|
17
|
+
| agent 作用域工具(Agent Teams 9 个) | `tool-agent-team` 用 `agent.ctx` 注册 | 把注册对象的 `when` 换成恒假,`resolve()` 的 admit 阶段剔除 | ~1.5K token |
|
|
18
|
+
| `team:identity`、`working-directory:current` | 同上 / `working-directory` | `required` 翻 false(全局那条在本层放 `required:false` 的影子) | ~90 token |
|
|
19
|
+
|
|
20
|
+
最后一项全部滤掉后,`joinContextSections([])` 是空串,`project()` 返回 `undefined`,那条 runtime-context user 消息整条不出现。注意**不能**在 `agent/pre-step` 里删它 —— agent-loop 在 append 之前会 `refreshContext` + `project` 重新算一遍,删的和追加的不是同一份。
|
|
21
|
+
|
|
22
|
+
## 开关
|
|
23
|
+
|
|
24
|
+
`lib/index.js` 顶部:
|
|
25
|
+
|
|
26
|
+
- `PRUNED_TOOL_NAMES` —— 要剪掉的 agent 作用域工具(默认是 Agent Teams 那 9 个,清空数组即可保留)
|
|
27
|
+
- `SUPPRESSED_CONTEXTS` —— 要滤掉的 runtime context 名(清空即保留 cwd / 团队身份)
|
|
28
|
+
- `noTools`(组合里 persona 行的 config)—— 关掉就不做全局工具 restrict
|
|
29
|
+
|
|
30
|
+
## 预期效果
|
|
31
|
+
|
|
32
|
+
同一 profile 下 `standard` 会话首步约 19K token;本预设约 **15–20 token**(你的那句话 + 服务端角色标记)。输出 token 不算在内 —— 那是模型自己想的。
|
|
33
|
+
|
|
34
|
+
## 备注
|
|
35
|
+
|
|
36
|
+
- `tools.restrict` / `when` / `required` 三处都是改注册对象本身(registry 返回的是真身,不是副本),不落盘;插件重载后由 `agent/created` 与 `turn/start` 重新施加。DSH 升级若改了内部结构,表现是"工具/上下文又冒出来",不会崩。
|
|
37
|
+
- 预设 id 是 `bare`。
|
package/cordis.patch.yml
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bare-preset(宿主)—— 把「裸模模式」这个 agent 预设置成一个插件。
|
|
3
|
+
*
|
|
4
|
+
* 为什么不再用预设目录 + bundle patch:
|
|
5
|
+
*
|
|
6
|
+
* • bundle patch 里的行要注册进 registry,registry 挂载时用的是它自己的
|
|
7
|
+
* baseUrl,`./persona-file.mjs` 这类相对路径解析不到(而写死绝对路径又
|
|
8
|
+
* 绑死目录);另外 id 必须是字符串、声明归属还要和「预设工作台」的运行时
|
|
9
|
+
* 声明抢所有权 —— 这一串坑都踩过了。
|
|
10
|
+
* • 现在这个包自己做主:激活时直接 `agentPresets.register()`,卸载时注销;
|
|
11
|
+
* 组合里的提示词行用包内子路径 `dsh-bare-preset/persona`,由 node 自己解析,
|
|
12
|
+
* 没有任何相对路径问题。
|
|
13
|
+
*
|
|
14
|
+
* 顺带一件事:`tools.restrict()` 只裁「继承来的」全局工具,而 Agent Teams 用
|
|
15
|
+
* `const scoped = agent.ctx` 把 9 个团队工具注册进 agent 自己那一层,按设计不受
|
|
16
|
+
* restrict 约束、也不能同名遮蔽(同层 duplicate)。但 `view().registrations` 给出
|
|
17
|
+
* 的是注册表里的真身(不是副本):把它的 `when` 换成恒假,`resolve()` 在 admit
|
|
18
|
+
* 阶段就会整个剔除 —— wire 上没有、系统提示的工具目录里也没有。
|
|
19
|
+
*
|
|
20
|
+
* 这个剪裁只对本预设的 agent 生效,其他会话(含团队协作)一点都不受影响。
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export const name = 'bare-preset'
|
|
24
|
+
|
|
25
|
+
/** 预设注册表(必需);tools / agents 由内层 inject 按可选依赖处理。 */
|
|
26
|
+
export const inject = ['agentPresets']
|
|
27
|
+
|
|
28
|
+
/** 预设 id。 */
|
|
29
|
+
const PRESET_ID = 'bare'
|
|
30
|
+
|
|
31
|
+
/** 本预设要剪掉的「agent 作用域工具」——目前就是 Agent Teams 那一套。 */
|
|
32
|
+
const PRUNED_TOOL_NAMES = [
|
|
33
|
+
'spawn_teammate',
|
|
34
|
+
'list_agents',
|
|
35
|
+
'send_message',
|
|
36
|
+
'wait_agent',
|
|
37
|
+
'interrupt_agent',
|
|
38
|
+
'team_task_create',
|
|
39
|
+
'team_task_get',
|
|
40
|
+
'team_task_list',
|
|
41
|
+
'team_task_update',
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 要让这个预设"看不到"的 runtime context 名字。
|
|
46
|
+
*
|
|
47
|
+
* `team:identity` 由 tool-agent-team 注册进 agent 本层;`working-directory:current`
|
|
48
|
+
* 由 working-directory 注册在全局层。把本层的条目标成 `required: false`(全局那条
|
|
49
|
+
* 则在本层放一个 required:false 的影子),装配末尾的
|
|
50
|
+
* `contexts.filter(entry => requiredContexts.has(entry.name))` 就会把它们整条丢掉。
|
|
51
|
+
*
|
|
52
|
+
* 为什么不能像工具那样在 `agent/pre-step` 里事后删消息:agent-loop 在真正 append
|
|
53
|
+
* 之前会**重新算一遍**(`refreshContext` + `runtimeContext.project`),所以那条
|
|
54
|
+
* runtime-context 消息根本不是 pre-step 里能摘掉的那一份。必须在装配阶段让它为空。
|
|
55
|
+
*/
|
|
56
|
+
const SUPPRESSED_CONTEXTS = ['team:identity', 'working-directory:current']
|
|
57
|
+
|
|
58
|
+
/** 恒假的 admit 条件;同一个函数对象也用来做幂等标记。 */
|
|
59
|
+
const DENIED = function () {
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 提示词行的绝对 file URL(组合里的行由 registry 在它自己的 baseUrl 下加载)。 */
|
|
64
|
+
const PERSONA_URL = new URL('../persona.mjs', import.meta.url).href
|
|
65
|
+
|
|
66
|
+
/** 交给 registry 的预设声明。 */
|
|
67
|
+
const DEFINITION = {
|
|
68
|
+
id: PRESET_ID,
|
|
69
|
+
name: '裸模模式',
|
|
70
|
+
description: '没有任何系统提示词,也看不到任何工具——只剩你说的话和模型本身。想观察模型的原生行为,或当作「从零开始」的底板。',
|
|
71
|
+
order: 6,
|
|
72
|
+
plugins: [
|
|
73
|
+
{
|
|
74
|
+
id: 'persona',
|
|
75
|
+
name: PERSONA_URL,
|
|
76
|
+
config: {
|
|
77
|
+
complete: true,
|
|
78
|
+
includeRuntimeContext: false,
|
|
79
|
+
noTools: true,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
// 长对话压缩:与 standard / bare 模板同一套栈,避免上下文无限增长。
|
|
84
|
+
id: 'compaction',
|
|
85
|
+
name: 'cordis:group',
|
|
86
|
+
group: true,
|
|
87
|
+
isolate: {
|
|
88
|
+
compaction: true,
|
|
89
|
+
},
|
|
90
|
+
config: [
|
|
91
|
+
{ id: 'compaction-basic', name: '@deepseek-ai/dsh-compaction-basic' },
|
|
92
|
+
{ id: 'command-compact', name: '@deepseek-ai/dsh-command-compact' },
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param ctx - 宿主插件上下文。
|
|
100
|
+
*/
|
|
101
|
+
export function apply(ctx) {
|
|
102
|
+
const presets = ctx.agentPresets
|
|
103
|
+
if (presets === undefined) return
|
|
104
|
+
|
|
105
|
+
// ── 1. 注册预设(卸载时注销)──────────────────────────────────────────
|
|
106
|
+
ctx.effect(() => {
|
|
107
|
+
let alive = true
|
|
108
|
+
let dispose
|
|
109
|
+
Promise.resolve()
|
|
110
|
+
.then(() => presets.register(DEFINITION))
|
|
111
|
+
.then((undo) => {
|
|
112
|
+
if (alive) {
|
|
113
|
+
dispose = undo
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
try { Promise.resolve(undo()).catch(() => {}) } catch (_) { /* ignore */ }
|
|
117
|
+
})
|
|
118
|
+
.catch((error) => {
|
|
119
|
+
try {
|
|
120
|
+
ctx.logger.warn('bare-preset: register failed: ' + (error && error.message ? error.message : String(error)))
|
|
121
|
+
} catch (_) { /* ignore */ }
|
|
122
|
+
})
|
|
123
|
+
return () => {
|
|
124
|
+
alive = false
|
|
125
|
+
if (typeof dispose !== 'function') return
|
|
126
|
+
try { Promise.resolve(dispose()).catch(() => {}) } catch (_) { /* ignore */ }
|
|
127
|
+
}
|
|
128
|
+
}, 'bare-preset.register()')
|
|
129
|
+
|
|
130
|
+
// ── 2. 剪掉别人塞进 agent 作用域的工具与团队身份行 ──────────────────────
|
|
131
|
+
ctx.inject(['tools', 'agents', 'systemPrompt'], (sctx) => {
|
|
132
|
+
const tools = sctx.get('tools')
|
|
133
|
+
const agentSvc = sctx.get('agents')
|
|
134
|
+
const prompt = sctx.get('systemPrompt')
|
|
135
|
+
if (tools === undefined || agentSvc === undefined) return
|
|
136
|
+
|
|
137
|
+
const isBare = function (agent) {
|
|
138
|
+
try {
|
|
139
|
+
return String(presets.composedPreset(agent.ctx) || '') === PRESET_ID
|
|
140
|
+
} catch (_) {
|
|
141
|
+
return false
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const prune = function (agent) {
|
|
146
|
+
if (agent === undefined || agent === null) return
|
|
147
|
+
if (!isBare(agent)) return
|
|
148
|
+
let view
|
|
149
|
+
try { view = tools.view(agent) } catch (_) { return }
|
|
150
|
+
for (const toolName of PRUNED_TOOL_NAMES) {
|
|
151
|
+
const registration = view.registrations.get(toolName)
|
|
152
|
+
if (registration === undefined || registration.when === DENIED) continue
|
|
153
|
+
registration.when = DENIED
|
|
154
|
+
}
|
|
155
|
+
suppressContexts(agent)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 已经在本层放过影子的 context 名(WeakMap<agent, Set<name>>)。 */
|
|
159
|
+
const shadowedContexts = new WeakMap()
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 让本 agent 的装配结果里不再出现 SUPPRESSED_CONTEXTS 列出的 runtime context。
|
|
163
|
+
*
|
|
164
|
+
* - `team:identity` 由 tool-agent-team 注册在 **agent 本层**:直接把 required 翻 false;
|
|
165
|
+
* - `working-directory:current` 注册在 **全局层**:不能全局改(会波及所有会话),
|
|
166
|
+
* 于是在 agent 本层放一个 `required: false` 的影子 —— merge 时本层优先,
|
|
167
|
+
* 装配末尾 `requiredContexts.has(name)` 就不认它了,working-directory 那条
|
|
168
|
+
* assemble 监听器事后补回去的贡献同样会被这一步滤掉。
|
|
169
|
+
*
|
|
170
|
+
* 全部被滤掉后,`joinContextSections([])` 得到空串,`project()` 在"从没有过快照"
|
|
171
|
+
* 的会话里直接返回 undefined —— 那条 runtime-context user 消息就不会出现。
|
|
172
|
+
*/
|
|
173
|
+
const suppressContexts = function (agent) {
|
|
174
|
+
const layers = prompt === undefined ? undefined : prompt.layers
|
|
175
|
+
if (layers === undefined || !(layers.scoped instanceof Map)) return
|
|
176
|
+
let layer
|
|
177
|
+
try { layer = layers.peek(agent) } catch (_) { return }
|
|
178
|
+
|
|
179
|
+
for (const contextName of SUPPRESSED_CONTEXTS) {
|
|
180
|
+
const own = layer === undefined || layer.contexts === undefined
|
|
181
|
+
? undefined
|
|
182
|
+
: layer.contexts.get(contextName)
|
|
183
|
+
if (own !== undefined) {
|
|
184
|
+
if (own.required !== false) own.required = false
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
const globalEntry = layers.global === undefined || layers.global.contexts === undefined
|
|
188
|
+
? undefined
|
|
189
|
+
: layers.global.contexts.get(contextName)
|
|
190
|
+
if (globalEntry === undefined) continue
|
|
191
|
+
let done = shadowedContexts.get(agent)
|
|
192
|
+
if (done === undefined) {
|
|
193
|
+
done = new Set()
|
|
194
|
+
shadowedContexts.set(agent, done)
|
|
195
|
+
}
|
|
196
|
+
if (done.has(contextName)) continue
|
|
197
|
+
done.add(contextName)
|
|
198
|
+
const actx = agent.ctx
|
|
199
|
+
if (actx === undefined || actx === null || actx.systemPrompt === undefined) continue
|
|
200
|
+
actx.effect(() => actx.systemPrompt.context({
|
|
201
|
+
...globalEntry,
|
|
202
|
+
required: false,
|
|
203
|
+
text: '',
|
|
204
|
+
}), 'bare-preset.shadowContext(' + contextName + ')')
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const pruneAll = function () {
|
|
209
|
+
let list = []
|
|
210
|
+
try { list = agentSvc.list() } catch (_) { return }
|
|
211
|
+
for (const agent of list) prune(agent)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
pruneAll()
|
|
215
|
+
|
|
216
|
+
// 团队工具也挂在 agent/created 上安装:等同步监听器跑完再剪,再补一刀延时兜底。
|
|
217
|
+
sctx.on('agent/created', function (event) {
|
|
218
|
+
const agent = event && event.agent
|
|
219
|
+
queueMicrotask(function () { prune(agent) })
|
|
220
|
+
setTimeout(function () { prune(agent) }, 120)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
// 最稳的一刀:turn/start 一定早于本次 step 的 tools.resolve()。
|
|
224
|
+
sctx.on('session/event', function (subject, event) {
|
|
225
|
+
if (event === undefined || event.type !== 'turn/start') return
|
|
226
|
+
let agent
|
|
227
|
+
try { agent = agentSvc.get(subject.id) } catch (_) { return }
|
|
228
|
+
prune(agent)
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-bare-preset",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "裸模模式:把「空系统提示词 + 零工具 + 零运行时上下文」这个 agent 预设置成一个插件 —— 自己向 agentPresets 注册,并剪掉宿主塞进 agent 作用域的工具与上下文。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js",
|
|
9
|
+
"./persona": "./persona.mjs",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"lib/index.js",
|
|
14
|
+
"persona.mjs",
|
|
15
|
+
"cordis.patch.yml",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/DDDMUC/dsh-bare-preset.git"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public",
|
|
25
|
+
"registry": "https://registry.npmjs.org"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"dsh",
|
|
29
|
+
"preset",
|
|
30
|
+
"agent-preset",
|
|
31
|
+
"bare",
|
|
32
|
+
"minimal-prompt"
|
|
33
|
+
],
|
|
34
|
+
"dsh": {
|
|
35
|
+
"engines": {
|
|
36
|
+
"dsh": ">=0.2.2-rc.1"
|
|
37
|
+
},
|
|
38
|
+
"bundle": {
|
|
39
|
+
"patch": "cordis.patch.yml"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
package/persona.mjs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bare-persona —— 裸模模式唯一的一条提示词行。
|
|
3
|
+
*
|
|
4
|
+
* 两件事:
|
|
5
|
+
*
|
|
6
|
+
* 1. 人设:用一个 `complete: true` 的空段落独占系统提示词,并关掉运行时快照,
|
|
7
|
+
* 于是渲染出来的系统消息是零个 content block —— 也就是"没有系统提示词"。
|
|
8
|
+
*
|
|
9
|
+
* 2. 工具表:`tools.restrict({ allow: [] })` 把继承来的(宿主/全局)工具全挡掉。
|
|
10
|
+
* 注意它管不到"注册进 agent 自己那一层"的工具(如 Agent Teams 的 9 个),
|
|
11
|
+
* 那部分由本包的宿主插件 lib/index.js 在每个 agent 创建/每轮开始前剪掉。
|
|
12
|
+
*
|
|
13
|
+
* 配置项(组合里传):complete / includeRuntimeContext / noTools / text。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const name = 'bare-persona'
|
|
17
|
+
|
|
18
|
+
/** 提示词注册表 + 工具视图。 */
|
|
19
|
+
export const inject = ['systemPrompt', 'tools']
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param ctx - 该预设的 agent 作用域。
|
|
23
|
+
* @param config - complete、includeRuntimeContext、noTools、text。
|
|
24
|
+
*/
|
|
25
|
+
export function apply(ctx, config = {}) {
|
|
26
|
+
const text = String(config.text ?? '')
|
|
27
|
+
const complete = config.complete ?? true
|
|
28
|
+
const includeRuntimeContext = config.includeRuntimeContext ?? false
|
|
29
|
+
const noTools = config.noTools ?? true
|
|
30
|
+
|
|
31
|
+
ctx.effect(() => ctx.systemPrompt.section({
|
|
32
|
+
name: 'deployment:persona',
|
|
33
|
+
order: 0,
|
|
34
|
+
text,
|
|
35
|
+
...(complete ? { complete: true } : {}),
|
|
36
|
+
}), 'bare-persona.section()')
|
|
37
|
+
|
|
38
|
+
if (!includeRuntimeContext) ctx.systemPrompt.suppressRuntimeContext()
|
|
39
|
+
|
|
40
|
+
// 空 allow = 这个作用域看不到任何继承来的工具;本作用域自己注册的不受影响。
|
|
41
|
+
if (noTools) ctx.effect(() => ctx.tools.restrict({ allow: [] }), 'bare-persona.restrict()')
|
|
42
|
+
}
|