dsh-plugin-tool-management 0.20.2 → 0.21.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/CHANGELOG.md +60 -1
- package/README.md +4 -1
- package/README_EN.md +4 -1
- package/docs/update.md +3 -1
- package/lib/client.js +225 -54
- package/lib/compat/capability-specs.js +60 -1
- package/lib/compat/host-contracts.js +72 -0
- package/lib/compat/host-probe.js +59 -0
- package/lib/compat/host-roots.js +1 -1
- package/lib/compat/probe.js +84 -13
- package/lib/context-inject-frame.js +134 -26
- package/lib/context-inject.js +12 -9
- package/lib/http-api.js +3 -2
- package/lib/http-fence.js +26 -1
- package/lib/index.js +5 -2
- package/lib/mcp/manager.js +6 -1
- package/lib/mcp/state-section.js +75 -34
- package/lib/memories/constants.js +39 -7
- package/lib/memories/projection.js +18 -18
- package/lib/memories/service.js +8 -7
- package/lib/memories/snapshot.js +27 -17
- package/lib/ops/compat.js +64 -2
- package/lib/ops/memory.js +2 -2
- package/lib/ops/prompts.js +30 -2
- package/lib/request-gate.js +5 -4
- package/lib/skills/catalog.js +2 -2
- package/lib/subagents/catalog.js +4 -4
- package/lib/subagents/child-preset.js +120 -0
- package/lib/subagents/persona-parse.js +9 -9
- package/lib/subagents/run-provider.js +218 -62
- package/lib/subagents/tool-filter.js +12 -12
- package/lib/subagents/tools.js +29 -22
- package/lib/tools/deps.js +4 -4
- package/lib/tools/mcp.js +34 -34
- package/lib/tools/memory.js +34 -34
- package/lib/tools/prompt.js +5 -5
- package/lib/tools/remove.js +17 -17
- package/lib/tools/scene.js +66 -66
- package/lib/tools/skills.js +21 -21
- package/lib/tools/subagent.js +13 -13
- package/lib/tools/table.js +2 -2
- package/package.json +25 -24
package/lib/subagents/tools.js
CHANGED
|
@@ -29,14 +29,14 @@ export async function filterBySceneBinding(docs, enabledSceneLists, includeUnava
|
|
|
29
29
|
if (includeUnavailable || !bound.size)
|
|
30
30
|
return { allowed: live, reason: null };
|
|
31
31
|
const allowed = live.filter((d) => bound.has(d.name));
|
|
32
|
-
return { allowed, reason: allowed.length ? null :
|
|
32
|
+
return { allowed, reason: allowed.length ? null : `the enabled scene's subagent bindings: ${[...bound].join(', ')}` };
|
|
33
33
|
}
|
|
34
34
|
export function defineSubagentManagerListTool(subagents) {
|
|
35
35
|
return {
|
|
36
36
|
name: 'subagent_manager_list',
|
|
37
37
|
// `depth-filtered` 删掉(2026-10-15 注入文本审查):注入深度是**注入侧**的实现细节,
|
|
38
38
|
// 模型看到的是"目录里有哪些人设",`catalogDepth` 那个词对它没有任何可操作性。
|
|
39
|
-
description: 'List personas (pre-configured subagent profiles) with their descriptions. The
|
|
39
|
+
description: 'List personas (pre-configured subagent profiles) with their descriptions. The "Current subagents" reminder carries a view of the same catalog; call this for the always-current full list, or pass all=true to include personas that are disabled or outside the active scene.',
|
|
40
40
|
parameters: {
|
|
41
41
|
all: { type: 'boolean', description: 'Include disabled personas and ones outside the active scene (default false). Listed ≠ delegable: `run` still refuses those.' },
|
|
42
42
|
},
|
|
@@ -54,9 +54,9 @@ export function defineSubagentManagerListTool(subagents) {
|
|
|
54
54
|
// 而工具又不列全,结果是"明明能委派却查不到人设"。
|
|
55
55
|
// `all=true` 时**标出哪些不可用**:只列名字的话,模型照样分不清哪个能直接委派
|
|
56
56
|
// (与技能列表标 `shadowed-by:` 同一条理由 —— 列出来不等于能用)。
|
|
57
|
-
const lines = allowed.map((p) => '- ' + p.name + ' — ' + (p.description || '(
|
|
57
|
+
const lines = allowed.map((p) => '- ' + p.name + ' — ' + (p.description || '(no description)') +
|
|
58
58
|
(p.continuable === true ? CONTINUABLE_MARK : '') +
|
|
59
|
-
(includeUnavailable ? (p.enabled === false ? '
|
|
59
|
+
(includeUnavailable ? (p.enabled === false ? ' (disabled — turn it on first)' : '') : ''));
|
|
60
60
|
// 默认口径下有多少个没列出来(停用的 + 不在启用场景里的):不报的话,模型看到一份短名单
|
|
61
61
|
// 会当成"本机只有这些人设"。数量照报,与另外三个 list 的表头同一口径。
|
|
62
62
|
const hiddenCount = includeUnavailable ? 0 : docs.length - allowed.length;
|
|
@@ -67,9 +67,9 @@ export function defineSubagentManagerListTool(subagents) {
|
|
|
67
67
|
catch {
|
|
68
68
|
notice = '';
|
|
69
69
|
}
|
|
70
|
-
const header = '
|
|
71
|
-
(hiddenCount > 0 ? '
|
|
72
|
-
return header + (lines.join('\n') || '(
|
|
70
|
+
const header = 'persona subagents (' + allowed.length + ')' +
|
|
71
|
+
(hiddenCount > 0 ? ' (' + hiddenCount + ' more are disabled or outside the current scene; pass all=true to see them)' : '') + ':\n';
|
|
72
|
+
return header + (lines.join('\n') || '(none)') + (reason ? '\nnote: ' + reason : '') + notice;
|
|
73
73
|
},
|
|
74
74
|
};
|
|
75
75
|
}
|
|
@@ -105,9 +105,9 @@ export function defineSubagentManagerRunTool(subagents) {
|
|
|
105
105
|
// 最贵的单个参数。收完两处重复后目标 ≈325 tok。判据是"删掉的那句在描述里已经有了吗":
|
|
106
106
|
// Modes 段留着(它是模型的入口),参数说明只留"这一段独有的信息"。
|
|
107
107
|
// `inherit` 那句"轮中委派拿不到当前轮"必须留 —— 它是这条参数唯一会让人写错 task 的地方。
|
|
108
|
-
description: 'Run a named persona as a subagent: it gets the persona as its own system prompt, works on `task`, and returns only its final output. A persona marked
|
|
108
|
+
description: 'Run a named persona as a subagent: it gets the persona as its own system prompt, works on `task`, and returns only its final output. A persona marked `(continuable)` instead returns an agent id and runs in the background — continue it with `send_message`.\n\nModes: by default a fresh child that cannot see this conversation, so `task` must be self-contained. With `inherit: true` it also gets this conversation\'s **finished** turns (like the host\'s `subagent_fork`) — the current turn is never included, so a mid-turn hand-off still needs a self-contained `task`.\n\n`task` = the goal plus the context it needs; leave method and output format to the persona.\n\nUse it when the work matches a persona in the "Current subagents" reminder (a review, an investigation, a piece of writing) and the detail should not sit in your own context. Not for reading a file (Read), finding a definition (Grep/Glob), or touching two or three files.',
|
|
109
109
|
parameters: {
|
|
110
|
-
agent: { type: 'string', required: true, description: 'Persona name — see the
|
|
110
|
+
agent: { type: 'string', required: true, description: 'Persona name — see the "Current subagents" reminder, or list personas with subagent_manager_list.' },
|
|
111
111
|
// `task` 的说明只留"这是什么"(2026-10-15 注入文本审查):上面描述里那句
|
|
112
112
|
// "leave method and output format to the persona." 此前在这里**逐字重复**了一遍
|
|
113
113
|
// (57 字符),而模型填这个参数时两处都在手上 —— 同一件事付两次 token。
|
|
@@ -123,22 +123,23 @@ export function defineSubagentManagerRunTool(subagents) {
|
|
|
123
123
|
const task = String((args && args.task) || '').trim();
|
|
124
124
|
const inherit = args && args.inherit === true;
|
|
125
125
|
if (!task)
|
|
126
|
-
throw new Error('task
|
|
126
|
+
throw new Error('task must not be empty');
|
|
127
127
|
// 官方 SubagentStartRequest.parent 必填:调用方 agent 缺失时给结构化错误,不把 undefined 透传下去。
|
|
128
128
|
if (!exec || !exec.agent)
|
|
129
|
-
throw new Error('
|
|
129
|
+
throw new Error('no caller agent (exec.agent is unavailable): cannot create a subagent run');
|
|
130
130
|
const docs = await subagents.list();
|
|
131
131
|
const { allowed, reason } = await filterBySceneBinding(docs, await subagents.sceneLists());
|
|
132
132
|
const persona = allowed.find((p) => p.name === name);
|
|
133
133
|
if (!persona) {
|
|
134
|
-
const available = allowed.map((p) => p.name).join('
|
|
135
|
-
throw new Error('
|
|
134
|
+
const available = allowed.map((p) => p.name).join(', ') || '(none)';
|
|
135
|
+
throw new Error('persona unavailable: ' + name + (reason ? ' (' + reason + ')' : ' (available: ' + available + ')'));
|
|
136
136
|
}
|
|
137
137
|
// 这里**刻意没有**"深度不够就拒绝"的检查(2026-09-17 用户裁定,方案 A)。
|
|
138
138
|
// 它此前基于一个错误假设:以为传 `maxDepth: 1` 会让子会话再委派必然失败。实际上官方
|
|
139
|
-
// `
|
|
140
|
-
//
|
|
141
|
-
//
|
|
139
|
+
// 的 `maxDepth` 只用**宿主配置**那一档(0.2.1 默认 1)、provider 只在**传了值**时才校验,
|
|
140
|
+
// 所以能不能继续委派问官方就行。那个检查的唯一净效果是让**我们的**工具比官方严 ——
|
|
141
|
+
// 同一个子会话里官方工具能委派、我们的被自己拦下 —— 而用户真想禁止嵌套也禁止不了
|
|
142
|
+
// (官方工具照样能)。
|
|
142
143
|
// `catalogDepth` 现在只管"目录注入到哪些会话",不参与委派可行性判断。
|
|
143
144
|
// 按当前会话的 Agent 预设算工具限制:判断不了当前预设、或名单里的工具已经不存在时,
|
|
144
145
|
// 结论里会带一句实话,跟着结果一起返回——不静默改变限制的强度。
|
|
@@ -149,14 +150,20 @@ export function defineSubagentManagerRunTool(subagents) {
|
|
|
149
150
|
const prefix = r.stopReason && r.stopReason !== 'completed' ? `[stopReason: ${r.stopReason}]\n` : '';
|
|
150
151
|
const note = decision && decision.note ? `⚠ ${decision.note}\n\n` : '';
|
|
151
152
|
// 允许追问通道(人设 `continuable: true`):产出是**异步**到达的,这里只拿到子会话 id。
|
|
152
|
-
// 回执必须把"id + 怎么追问 + 怎么中止"
|
|
153
|
-
// 读成"委派失败"而重试一次(那会再起一个后台子代理,白白翻倍)。
|
|
153
|
+
// 回执必须把"id + 产出怎么回来 + 怎么追问 + 怎么中止"写全 —— 少写一句,模型就会把
|
|
154
|
+
// "没拿到结果"读成"委派失败"而重试一次(那会再起一个后台子代理,白白翻倍)。
|
|
155
|
+
// "产出怎么回来"说的是**宿主的结算通知**(0.2.1 核实 `dsh-subagent/lib/types/manager.js`
|
|
156
|
+
// 的 `notifySettlement` → `createSettlementMessage`):子代理结束时父代理会收到一条
|
|
157
|
+
// `subagent-settled` 通知,含收尾一行说明 + 子代理最终的非空 text 块(+ 结构化结果与
|
|
158
|
+
// 诊断)。它自己发回来的 `send_message` 是**补充**(`withContinuableReturnGuidance`
|
|
159
|
+
// 要求它在收尾前把自包含结果发给父代理),不是产出的唯一来路 —— 回执不能只说前者,
|
|
160
|
+
// 否则子代理没主动发消息时父代理会把"还没收到结果"当成"还在跑"。
|
|
154
161
|
if (r.continuable === true) {
|
|
155
|
-
return note + '
|
|
162
|
+
return note + 'started the continuable subagent "' + name + '" (running in the background)\n'
|
|
156
163
|
+ 'agent_id: ' + r.runId + '\n\n'
|
|
157
|
-
+ '
|
|
158
|
-
+ 'send_message({ agent_id: "' + r.runId + '", message: "..." })
|
|
159
|
-
+ '
|
|
164
|
+
+ 'you will get a notification carrying its final output when it settles. To follow up: '
|
|
165
|
+
+ 'send_message({ agent_id: "' + r.runId + '", message: "..." }), '
|
|
166
|
+
+ 'To stop it: interrupt_agent({ agent_id: "' + r.runId + '" }).';
|
|
160
167
|
}
|
|
161
168
|
return note + prefix + r.text;
|
|
162
169
|
},
|
package/lib/tools/deps.js
CHANGED
|
@@ -7,7 +7,7 @@ export function text(value) {
|
|
|
7
7
|
return [{ type: 'text', text: value }];
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* 名单渲染:超过 `TOOL_NAME_LIST_CAP` 项就截断,并把总数写在后面("
|
|
10
|
+
* 名单渲染:超过 `TOOL_NAME_LIST_CAP` 项就截断,并把总数写在后面("…(N total)")。
|
|
11
11
|
*
|
|
12
12
|
* 为什么两个地方都要它:`scene_manager_list` 用它列档案,`scene_manager_save` 用它
|
|
13
13
|
* 在回执里回显"这一次替换挤掉了什么"。两处都必须**点名、不能只给数量** —— 只给数量
|
|
@@ -17,9 +17,9 @@ export function text(value) {
|
|
|
17
17
|
export const TOOL_NAME_LIST_CAP = 6;
|
|
18
18
|
export function toolNameList(list) {
|
|
19
19
|
if (!list.length)
|
|
20
|
-
return '
|
|
21
|
-
const head = list.slice(0, TOOL_NAME_LIST_CAP).join('
|
|
22
|
-
return list.length > TOOL_NAME_LIST_CAP ? head + '
|
|
20
|
+
return 'none';
|
|
21
|
+
const head = list.slice(0, TOOL_NAME_LIST_CAP).join(', ');
|
|
22
|
+
return list.length > TOOL_NAME_LIST_CAP ? head + ' ...(' + list.length + ' total)' : head;
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
25
|
* 按名字找技能落在哪个来源(root)。
|
package/lib/tools/mcp.js
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
import { DEFAULT_MCP_NOTE_MAX_LENGTH, normalizeMcpNote } from '../mcp/state-section.js';
|
|
29
29
|
import { text } from './deps.js';
|
|
30
30
|
/** 「去看清单」那句:`mcp_manager_list` 不在模型手里时不能点名它(调用方按 `toolVisibleFor` 决定给不给)。 */
|
|
31
|
-
const MCP_LIST_HINT = '
|
|
31
|
+
const MCP_LIST_HINT = ' (see mcp_manager_list — all=true also lists disabled ones)';
|
|
32
32
|
/**
|
|
33
33
|
* 指不到 `mcp_manager_list` 时的**中性出口**:不点名任何工具,两条常驻来源都成立
|
|
34
34
|
* —— 模型自己的 `mcp__<server>__<tool>` 工具名(就在它的 schema 里),以及注入段。
|
|
@@ -37,22 +37,22 @@ const MCP_LIST_HINT = '(用 mcp_manager_list 看清单,all=true 连停用的
|
|
|
37
37
|
* 的 MCP 工具,猜错 serverName 时若报错里只有「没有这台服务器:X」,模型拿不到任何补救线索
|
|
38
38
|
* —— 它只能猜第二次。出口要么指向工具(那条工具得在它手里),要么指向这两条常驻来源。
|
|
39
39
|
*/
|
|
40
|
-
const MCP_NEUTRAL_HINT = '
|
|
40
|
+
const MCP_NEUTRAL_HINT = ' (copy the server name from your own `mcp__<server>__<tool>` tool names, or take one listed in the "Current MCP servers" reminder)';
|
|
41
41
|
/** 按 serverName 定位条目;同名(project + global 各一台)时要求 level 明确指定。 */
|
|
42
42
|
function locate(rows, server, level, listHint = MCP_LIST_HINT) {
|
|
43
43
|
const hits = (rows || []).filter((r) => String(r.serverName) === server);
|
|
44
44
|
if (!hits.length) {
|
|
45
|
-
throw new Error('
|
|
45
|
+
throw new Error('no such server: ' + server + (listHint || MCP_NEUTRAL_HINT));
|
|
46
46
|
}
|
|
47
47
|
if (level) {
|
|
48
48
|
const picked = hits.filter((r) => r.level === level);
|
|
49
49
|
if (!picked.length) {
|
|
50
|
-
throw new Error('
|
|
50
|
+
throw new Error('"' + server + '" has no level=' + level + ' entry (existing: ' + hits.map((r) => r.level).join(' / ') + ')');
|
|
51
51
|
}
|
|
52
52
|
return picked[0];
|
|
53
53
|
}
|
|
54
54
|
if (hits.length > 1) {
|
|
55
|
-
throw new Error('
|
|
55
|
+
throw new Error('"' + server + '" exists at both ' + hits.map((r) => r.level).join(' and ') + ' — pass level to say which one to touch');
|
|
56
56
|
}
|
|
57
57
|
return hits[0];
|
|
58
58
|
}
|
|
@@ -89,11 +89,11 @@ export function buildMcpTools(deps) {
|
|
|
89
89
|
// 是行动依据 —— 模型要知道"停用的那台去哪查"就得靠这句(all=true)。
|
|
90
90
|
//
|
|
91
91
|
// 2026-10-15 注入文本审查:删掉两处白付的 token ——
|
|
92
|
-
// ①
|
|
92
|
+
// ① 括号里那句把 "Current MCP servers" reminder 的**内容**逐项列了一遍(可用 / 曾连上 /
|
|
93
93
|
// 备注 / 停用不进),而那段 reminder 每轮就在模型上下文里,它自己看得到;
|
|
94
94
|
// ② `where loader:on:<phase> is the loader's current phase` —— 用自己解释自己
|
|
95
95
|
// (phase 就是 phase),零信息。字段名本身留在输出里,模型照抄即可。
|
|
96
|
-
description: 'List configured MCP servers (level project|global; enabled state; live loader status; tool count excluding switched-off tools; user hint). The
|
|
96
|
+
description: 'List configured MCP servers (level project|global; enabled state; live loader status; tool count excluding switched-off tools; user hint). The "Current MCP servers" reminder is a filtered view; this tool is the raw one — defaults to enabled servers, all=true for every configured one. Pass tools=true to list each server\'s tool names.',
|
|
97
97
|
parameters: {
|
|
98
98
|
all: { type: 'boolean', description: 'Include disabled servers (default false).' },
|
|
99
99
|
tools: { type: 'boolean', description: 'Also list each server\'s tool names, marking the switched-off ones (default false).' },
|
|
@@ -162,7 +162,7 @@ export function buildMcpTools(deps) {
|
|
|
162
162
|
// 报错退化成「没有这台服务器:<你猜的>」—— 一个字都不告诉它还能去哪看。
|
|
163
163
|
// 指向模型自己的工具表(`mcp__<server>__<tool>` 就在它的 schema 里)与常驻注入段,
|
|
164
164
|
// 这两条**不需要任何工具可见**就成立。
|
|
165
|
-
server: { type: 'string', required: true, description: 'Server name, as it appears in your own `mcp__<server>__<tool>` tool names (the
|
|
165
|
+
server: { type: 'string', required: true, description: 'Server name, as it appears in your own `mcp__<server>__<tool>` tool names (the "Current MCP servers" reminder lists the usable ones).' },
|
|
166
166
|
action: { type: 'string', required: true, enum: ['on', 'off', 'restart'], description: 'on = enable, off = disable, restart = reconnect and re-sync tools.' },
|
|
167
167
|
level: { type: 'string', description: 'project or global — only to disambiguate same-name servers.' },
|
|
168
168
|
tool: { type: 'string', description: 'One tool name on that server (no mcp__ prefix). Only for on/off.' },
|
|
@@ -171,12 +171,12 @@ export function buildMcpTools(deps) {
|
|
|
171
171
|
async execute(args, exec) {
|
|
172
172
|
const action = String((args && args.action) || '');
|
|
173
173
|
if (action !== 'on' && action !== 'off' && action !== 'restart') {
|
|
174
|
-
throw new Error('action
|
|
174
|
+
throw new Error('action must be on / off / restart (got: ' + (action || '(empty)') + ')');
|
|
175
175
|
}
|
|
176
176
|
const server = String((args && args.server) || '').trim();
|
|
177
177
|
const tool = args && args.tool !== undefined ? String(args.tool).trim() : '';
|
|
178
178
|
if (tool !== '' && action === 'restart') {
|
|
179
|
-
throw new Error('
|
|
179
|
+
throw new Error('restart is a whole-server action (off, wait for it to drop, then back on at its previous state); a single tool cannot be restarted');
|
|
180
180
|
}
|
|
181
181
|
// on|off 会写补丁(`mcpm-set-enabled` / `mcpm-tool-enabled` 在登记表里是 frozen),
|
|
182
182
|
// 所以过场景冻结守卫;restart 不过 —— 它是锁定期间唯一的恢复入口(见文件头)。
|
|
@@ -187,36 +187,36 @@ export function buildMcpTools(deps) {
|
|
|
187
187
|
}
|
|
188
188
|
const list = await deps.mcpmListView();
|
|
189
189
|
if (!list || list.ok === false)
|
|
190
|
-
throw new Error((list && list.error) || '
|
|
190
|
+
throw new Error((list && list.error) || 'failed to read the MCP config');
|
|
191
191
|
const level = args && args.level !== undefined ? String(args.level) : undefined;
|
|
192
192
|
const row = locate(list.rows || [], server, level, listHint(exec && exec.agent));
|
|
193
193
|
const enabled = action === 'on';
|
|
194
194
|
if (tool !== '') {
|
|
195
195
|
const r = await deps.mcpmToolEnabled({ serverName: String(row.serverName), tool, enabled });
|
|
196
196
|
if (!r || r.ok === false)
|
|
197
|
-
throw new Error((r && r.error) || '
|
|
197
|
+
throw new Error((r && r.error) || 'failed to switch this tool');
|
|
198
198
|
// 与面板同一条同步:`mcpm-tool-enabled` 在登记表里带 syncsArchive,漏了就是
|
|
199
199
|
// "页面拨的进档案、模型拨的不进"。
|
|
200
200
|
const syncErr = await deps.syncSwitchToScene('mcpm-tool-enabled', { serverName: String(row.serverName), tool, enabled });
|
|
201
201
|
// 整台停用时,工具级开关在运行时本来就不生效(那个工具压根不存在)—— 不说的话
|
|
202
202
|
// 模型会以为已经生效。与记忆族"启用了一条不在启用场景里的记忆"同一条理由。
|
|
203
|
-
const offNote = row.disabled ? '
|
|
203
|
+
const offNote = row.disabled ? ' (note: the whole server is off right now, so this tool-level switch only takes effect once the server is on)' : '';
|
|
204
204
|
return 'OK: ' + String(row.serverName) + ' tool ' + tool + ' now ' + (enabled ? 'enabled' : 'disabled') + offNote +
|
|
205
|
-
(syncErr ? '\nWARN:
|
|
205
|
+
(syncErr ? '\nWARN: the current scene archive was not synced (' + syncErr + ')' : '');
|
|
206
206
|
}
|
|
207
207
|
if (action === 'restart') {
|
|
208
208
|
const r = await deps.mcpmRestart({ id: row.id, level: row.level });
|
|
209
209
|
if (!r || r.ok === false)
|
|
210
|
-
throw new Error((r && r.error) || '
|
|
210
|
+
throw new Error((r && r.error) || 'restart failed');
|
|
211
211
|
return 'OK: ' + String(row.serverName) + ' (' + row.level + ') restarted';
|
|
212
212
|
}
|
|
213
213
|
const r = await deps.mcpmSetEnabled({ id: row.id, level: row.level, enabled });
|
|
214
214
|
if (!r || r.ok === false)
|
|
215
|
-
throw new Error((r && r.error) || '
|
|
215
|
+
throw new Error((r && r.error) || 'failed to switch this server');
|
|
216
216
|
// 场景未锁定时,页面 / 模型改的开关都要落进当前场景的档案(与 handlers 层同一套同步)。
|
|
217
217
|
const syncErr = await deps.syncSwitchToScene('mcpm-set-enabled', { id: row.id, level: row.level, enabled });
|
|
218
218
|
return 'OK: ' + String(row.serverName) + ' (' + row.level + ') now ' + (enabled ? 'enabled' : 'disabled') +
|
|
219
|
-
(syncErr ? '\nWARN:
|
|
219
|
+
(syncErr ? '\nWARN: the current scene archive was not synced (' + syncErr + ')' : '');
|
|
220
220
|
},
|
|
221
221
|
}));
|
|
222
222
|
register(defineTool({
|
|
@@ -232,7 +232,7 @@ export function buildMcpTools(deps) {
|
|
|
232
232
|
env: { type: 'string', description: 'Extra env vars, key=value per line (stdio). Omit = keep.' },
|
|
233
233
|
level: { type: 'string', description: 'project or global (project when creating); only needed for same-name servers.' },
|
|
234
234
|
// 名字与注入段对齐(2026-10-14 对抗性审查 P1-2):注入段与工具输出都写 `user-hint`
|
|
235
|
-
//
|
|
235
|
+
// (对应注入行里那句 `user hint:`),参数名跟着叫 `hint`,一份数据三个面一个词。
|
|
236
236
|
// 「写什么」那半句已经由注入框架的 `how` 行逐字说过("选服务器之前先看一眼"),
|
|
237
237
|
// 这里只留参数独有的信息:长度上限与截断行为(2026-10-14 审查 P2-4)。
|
|
238
238
|
hint: { type: 'string', description: "Short hint for later (≤300 chars; longer is stored whole but truncated in listings). '' clears the existing hint; omit = keep." },
|
|
@@ -244,7 +244,7 @@ export function buildMcpTools(deps) {
|
|
|
244
244
|
throw new Error(blocked);
|
|
245
245
|
const server = String((args && args.server) || '').trim();
|
|
246
246
|
if (!server)
|
|
247
|
-
throw new Error('
|
|
247
|
+
throw new Error('missing argument: server (the server name)');
|
|
248
248
|
const levelArg = args && args.level !== undefined ? String(args.level) : undefined;
|
|
249
249
|
// `hint` 的三态(2026-10-15 修 P1-5):省略 = 保持;`''` = **清空**;非空 = 设置。
|
|
250
250
|
// 此前用 `hintText.trim() !== ''` 判"要不要写备注",把"清空"与"省略"混成一件事 ——
|
|
@@ -255,20 +255,20 @@ export function buildMcpTools(deps) {
|
|
|
255
255
|
const clearsNote = hintGiven && hintText.trim() === '';
|
|
256
256
|
const list = await deps.mcpmListView();
|
|
257
257
|
if (!list || list.ok === false)
|
|
258
|
-
throw new Error((list && list.error) || '
|
|
258
|
+
throw new Error((list && list.error) || 'failed to read the MCP config');
|
|
259
259
|
const rows = list.rows || [];
|
|
260
260
|
const hits = rows.filter((r) => String(r.serverName) === server);
|
|
261
261
|
// ── 建:transport 与它那一侧的必填项都得给 ─────────────────────────────
|
|
262
262
|
if (!hits.length) {
|
|
263
263
|
const transport = String((args && args.transport) || '').trim();
|
|
264
264
|
if (transport !== 'stdio' && transport !== 'streamable-http') {
|
|
265
|
-
throw new Error('
|
|
265
|
+
throw new Error('creating a server needs transport: stdio or streamable-http');
|
|
266
266
|
}
|
|
267
267
|
if (transport === 'stdio' && String((args && args.command) || '').trim() === '') {
|
|
268
|
-
throw new Error('
|
|
268
|
+
throw new Error('a new stdio server needs command (the host spawns that process)');
|
|
269
269
|
}
|
|
270
270
|
if (transport === 'streamable-http' && String((args && args.url) || '').trim() === '') {
|
|
271
|
-
throw new Error('
|
|
271
|
+
throw new Error('a new streamable-http server needs url');
|
|
272
272
|
}
|
|
273
273
|
const payload = {
|
|
274
274
|
serverName: server,
|
|
@@ -283,14 +283,14 @@ export function buildMcpTools(deps) {
|
|
|
283
283
|
payload.level = levelArg;
|
|
284
284
|
const r = await deps.mcpmAdd(payload);
|
|
285
285
|
if (!r || r.ok === false)
|
|
286
|
-
throw new Error((r && r.error) || '
|
|
286
|
+
throw new Error((r && r.error) || 'failed to add the server');
|
|
287
287
|
// 备注单独走 `mcpm-note`(`mcpm-add` 不认这个字段)。失败**不回滚**主写入:
|
|
288
288
|
// 服务器已经建好了,"备注没写上"是 warning,不是失败(与 0.13.0 界面同规格)。
|
|
289
289
|
let warn = '';
|
|
290
290
|
if (hintGiven && !clearsNote) {
|
|
291
291
|
const n = await deps.mcpmNote({ id: String(r.row.id), note: hintText });
|
|
292
292
|
if (!n || n.ok === false)
|
|
293
|
-
warn = '\nWARN:
|
|
293
|
+
warn = '\nWARN: the server was created, but the hint was not written (' + ((n && n.error) || 'unknown reason') + ')';
|
|
294
294
|
}
|
|
295
295
|
return 'OK: created ' + r.row.id + ' at ' + r.row.level + warn;
|
|
296
296
|
}
|
|
@@ -298,7 +298,7 @@ export function buildMcpTools(deps) {
|
|
|
298
298
|
const cur = locate(hits, server, levelArg, listHint(exec && exec.agent));
|
|
299
299
|
const transport = args && args.transport !== undefined ? String(args.transport) : String(cur.transport);
|
|
300
300
|
if (transport !== 'stdio' && transport !== 'streamable-http') {
|
|
301
|
-
throw new Error('transport
|
|
301
|
+
throw new Error('transport must be stdio or streamable-http (this one is: ' + String(cur.transport) + ')');
|
|
302
302
|
}
|
|
303
303
|
const touched = [];
|
|
304
304
|
const ignored = [];
|
|
@@ -365,15 +365,15 @@ export function buildMcpTools(deps) {
|
|
|
365
365
|
// 直接写备注。这与"三个可改字段一个都没给就拒"是同一条理由的两面。
|
|
366
366
|
if (!touched.length && !levelChanged) {
|
|
367
367
|
if (!hintGiven)
|
|
368
|
-
throw new Error('
|
|
368
|
+
throw new Error('nothing to change: give at least one of transport / url / command / args / headers / env / level / hint');
|
|
369
369
|
const n = await deps.mcpmNote({ id: cur.id, note: hintText });
|
|
370
370
|
if (!n || n.ok === false)
|
|
371
|
-
throw new Error((n && n.error) || '
|
|
372
|
-
return 'OK: updated ' + String(cur.serverName) + '
|
|
371
|
+
throw new Error((n && n.error) || 'failed to write the hint');
|
|
372
|
+
return 'OK: updated ' + String(cur.serverName) + ' (' + (clearsNote ? 'hint cleared' : 'hint changed') + ')';
|
|
373
373
|
}
|
|
374
374
|
const r = await deps.mcpmEdit(payload);
|
|
375
375
|
if (!r || r.ok === false)
|
|
376
|
-
throw new Error((r && r.error) || '
|
|
376
|
+
throw new Error((r && r.error) || 'failed to update the server');
|
|
377
377
|
// `hint` 进 `changed` 必须**等写成功之后**(2026-10-15 修 P1-6):此前按意图先算进去,
|
|
378
378
|
// mcpmNote 失败时回执一边说"改了 hint"、一边 WARN"备注没写进去",模型读不出到底改没改。
|
|
379
379
|
const changed = touched.concat(levelChanged ? ['level'] : []);
|
|
@@ -385,14 +385,14 @@ export function buildMcpTools(deps) {
|
|
|
385
385
|
if (hintGiven) {
|
|
386
386
|
const n = await deps.mcpmNote({ id: cur.id, note: hintText });
|
|
387
387
|
if (!n || n.ok === false)
|
|
388
|
-
warn += '\nWARN:
|
|
388
|
+
warn += '\nWARN: the server was saved, but the hint was not written (' + ((n && n.error) || 'unknown reason') + ')';
|
|
389
389
|
else
|
|
390
|
-
changed.push(clearsNote ? 'hint
|
|
390
|
+
changed.push(clearsNote ? 'hint (cleared)' : 'hint');
|
|
391
391
|
}
|
|
392
392
|
if (ignored.length) {
|
|
393
|
-
warn += '\nWARN: ' + ignored.join(' / ') + '
|
|
393
|
+
warn += '\nWARN: ' + ignored.join(' / ') + ' do not apply to the current transport (' + transport + ') and were ignored';
|
|
394
394
|
}
|
|
395
|
-
return 'OK: updated ' + String(cur.serverName) + '
|
|
395
|
+
return 'OK: updated ' + String(cur.serverName) + ' (changed ' + changed.join(', ') + ')' + warn;
|
|
396
396
|
},
|
|
397
397
|
}));
|
|
398
398
|
}
|
package/lib/tools/memory.js
CHANGED
|
@@ -26,7 +26,7 @@ export function buildMemoryTools(deps) {
|
|
|
26
26
|
name: 'memory_manager_list',
|
|
27
27
|
// 末尾**不点名** `scene_manager_list`:场景一族在出厂默认里就是关着的,指向一条模型没有的
|
|
28
28
|
// 工具只会让它去猜名字(用户 2026-09-23 把默认名单扩到十五条时去掉的这句)。
|
|
29
|
-
description: 'List memories (id, scene, enabled, form flat|bundle, description) plus the scene injection currently follows. What is injected each turn is in the
|
|
29
|
+
description: 'List memories (id, scene, enabled, form flat|bundle, description) plus the scene injection currently follows. What is injected each turn is in the "Current memories" reminder; use this for ids and for entries that are off. Pass a `scene` to filter by scene; all=true lists everything.',
|
|
30
30
|
parameters: {
|
|
31
31
|
// 参数名与其余五个域同词(2026-10-14 对抗性审查 P1-3):同一个概念在工具面只该有一个名字,
|
|
32
32
|
// 而 `scene` 是它在 `scene_manager_*`、`<scene>/<name>` 与注入段标题里的叫法。
|
|
@@ -44,7 +44,7 @@ export function buildMemoryTools(deps) {
|
|
|
44
44
|
opArgs.group = args.scene;
|
|
45
45
|
const r = await deps.rulesOps['rules-list'](opArgs);
|
|
46
46
|
if (!r || r.ok === false)
|
|
47
|
-
throw new Error((r && r.error) || '
|
|
47
|
+
throw new Error((r && r.error) || 'failed to read memories');
|
|
48
48
|
// 「会注入」的判定与记忆段同源(renderSceneMemory):global 场景恒常注入,其余场景要
|
|
49
49
|
// 在 index.active 里,再叠加单条 enabled 与同名 bundle 的 shadowed。默认只列这些,
|
|
50
50
|
// 全列一次会把没启用的条目也算进上下文(用户裁定 2026-09-16,与技能列表同口径)。
|
|
@@ -52,37 +52,37 @@ export function buildMemoryTools(deps) {
|
|
|
52
52
|
(group !== '' && (r.activeMode === 'all' || String(r.activeScene || '') === group));
|
|
53
53
|
const stateOf = (x) => {
|
|
54
54
|
if (x.shadowed === true)
|
|
55
|
-
return { injects: false, label: '
|
|
55
|
+
return { injects: false, label: 'shadowed' };
|
|
56
56
|
if (x.enabled === false)
|
|
57
|
-
return { injects: false, label: '
|
|
57
|
+
return { injects: false, label: 'disabled' };
|
|
58
58
|
if (!sceneOn(String(x.group || '')))
|
|
59
|
-
return { injects: false, label: '
|
|
60
|
-
return { injects: true, label: '
|
|
59
|
+
return { injects: false, label: 'scene off' };
|
|
60
|
+
return { injects: true, label: 'injects' };
|
|
61
61
|
};
|
|
62
62
|
const rows = (r.rules || []).map((x) => ({ x, ...stateOf(x) }));
|
|
63
63
|
const shown = showAll ? rows : rows.filter((row) => row.injects);
|
|
64
|
-
const lines = shown.map(({ x, label }) => ('- ' + x.id + ' [' + (x.group || '
|
|
64
|
+
const lines = shown.map(({ x, label }) => ('- ' + x.id + ' [' + (x.group || 'no scene') + '] ' + label +
|
|
65
65
|
// 形态与附件(2026-10-15):此前不报,模型分不清哪条是 bundle、更不知道它带了几个附件
|
|
66
66
|
// (`rules-list` 早就把 `form` 与 bundle 的附件摘要给了出来,只是这里没渲染)。
|
|
67
67
|
(x.form === 'bundle'
|
|
68
|
-
? ' · bundle' + (x.attach && x.attach.count ? '
|
|
68
|
+
? ' · bundle' + (x.attach && x.attach.count ? ' (' + x.attach.count + ' attachment(s))' : '')
|
|
69
69
|
: '') +
|
|
70
70
|
(x.description ? ' — ' + x.description : '')));
|
|
71
71
|
// 注入按哪个场景走 —— 上面那句「场景未启用」只有配上它才可解读:少了这一行,
|
|
72
72
|
// 模型看到一片"未启用"会以为是条目自己的问题。(场景本身的清单在 `scene_manager_list`,
|
|
73
73
|
// 2026-09-23 拆过去了;这里只留"记忆按哪一集注入",因为那是记忆域自己的现状。)
|
|
74
74
|
const scope = r.activeMode === 'all'
|
|
75
|
-
? '
|
|
76
|
-
: (r.activeScene ? '
|
|
77
|
-
const header = '
|
|
78
|
-
? rows.length + '
|
|
79
|
-
: shown.length + '
|
|
80
|
-
(shown.length === rows.length ? '' : '
|
|
81
|
-
'\
|
|
75
|
+
? 'every scene (historical default, not narrowed)'
|
|
76
|
+
: (r.activeScene ? '"' + String(r.activeScene) + '"' : 'no scene is on');
|
|
77
|
+
const header = 'memories: ' + (showAll
|
|
78
|
+
? rows.length + ' total'
|
|
79
|
+
: shown.length + ' injecting / ' + rows.length + ' total' +
|
|
80
|
+
(shown.length === rows.length ? '' : ' (pass all=true for all)')) +
|
|
81
|
+
'\ninjects from: ' + scope + ' + global (global always injects)\n';
|
|
82
82
|
// 注入边界:压制型预设(persona complete / 关闭运行时上下文)下本插件默认不注入,
|
|
83
83
|
// 此时列出的记忆**不在**模型上下文里。必须说出来,否则模型会假设自己已经看到正文。
|
|
84
84
|
const notice = await deps.reachNoticeForAgent(deps.presetRoster(), exec && exec.agent && exec.agent.ctx, deps.injectNoticeOptions());
|
|
85
|
-
return header + (lines.join('\n') || '(
|
|
85
|
+
return header + (lines.join('\n') || '(none)') + notice;
|
|
86
86
|
},
|
|
87
87
|
}));
|
|
88
88
|
register(defineTool({
|
|
@@ -99,7 +99,7 @@ export function buildMemoryTools(deps) {
|
|
|
99
99
|
// op 的字段名是 `id`,不改 op:这里显式转换(键名对模型是 `<场景>/<名字>`,对 op 是 `id`)。
|
|
100
100
|
const r = await deps.rulesOps['rules-read']({ id: String((args && args.memory) || '') });
|
|
101
101
|
if (!r || r.ok === false)
|
|
102
|
-
throw new Error((r && r.error) || '
|
|
102
|
+
throw new Error((r && r.error) || 'failed to read memories');
|
|
103
103
|
const rule = r.rule || {};
|
|
104
104
|
// 形态与附件名(2026-10-15):op 一直把 `attachments` 给了出来,这里此前**整个丢掉**,
|
|
105
105
|
// 于是模型连"这条记忆是 bundle、带了 3 个附件"都看不到。只给名字不给内容 ——
|
|
@@ -107,9 +107,9 @@ export function buildMemoryTools(deps) {
|
|
|
107
107
|
const attach = Array.isArray(rule.attachments) ? rule.attachments : [];
|
|
108
108
|
const form = rule.form === 'bundle' ? 'bundle' : 'flat';
|
|
109
109
|
const attLine = attach.length
|
|
110
|
-
? '\
|
|
110
|
+
? '\nattachments (' + attach.length + '): ' + attach.map((a) => String((a && a.name) || a)).join(', ')
|
|
111
111
|
: '';
|
|
112
|
-
return '# ' + rule.id + '
|
|
112
|
+
return '# ' + rule.id + ' (' + form + ')' + attLine + '\n\n' + (rule.body || '');
|
|
113
113
|
},
|
|
114
114
|
}));
|
|
115
115
|
register(defineTool({
|
|
@@ -143,10 +143,10 @@ export function buildMemoryTools(deps) {
|
|
|
143
143
|
// `enabled` 必须是布尔:`rules-toggle` 缺了它直接拒(ops/memory.ts),但那是给用户看的
|
|
144
144
|
// 中文 op 错;这里先挡一句说清"这条工具要什么"。
|
|
145
145
|
if (typeof (args && args.enabled) !== 'boolean')
|
|
146
|
-
throw new Error('
|
|
146
|
+
throw new Error('missing argument: enabled must be true or false');
|
|
147
147
|
const r = await deps.rulesOps['rules-toggle']({ id: memory, enabled: args.enabled });
|
|
148
148
|
if (!r || r.ok === false)
|
|
149
|
-
throw new Error((r && r.error) || '
|
|
149
|
+
throw new Error((r && r.error) || 'failed to switch this memory');
|
|
150
150
|
const rule = r.rule || {};
|
|
151
151
|
// 「启用了一条不在启用场景里的记忆」是一次静默无效:单条开关拨上去了,注入集里却没有它。
|
|
152
152
|
// 这一句不说,模型会以为已经生效 —— 与 skill_manager_switch 说清影子副本同一条理由。
|
|
@@ -157,7 +157,7 @@ export function buildMemoryTools(deps) {
|
|
|
157
157
|
const group = String(rule.group || '');
|
|
158
158
|
const on = group === 'global' || (group !== '' && (all.activeMode === 'all' || String(all.activeScene || '') === group));
|
|
159
159
|
if (!on)
|
|
160
|
-
sceneNote = ' — BUT its scene
|
|
160
|
+
sceneNote = ' — BUT its scene "' + group + '" is not the one injecting, so it still will not be injected; turn that scene on (or move it to global) first';
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
catch {
|
|
@@ -193,7 +193,7 @@ export function buildMemoryTools(deps) {
|
|
|
193
193
|
// 清单里打印的就是 `<场景>/<名字>`,照抄比拼装准(§2 规则一)。
|
|
194
194
|
const slash = memory.indexOf('/');
|
|
195
195
|
if (slash <= 0 || slash === memory.length - 1) {
|
|
196
|
-
throw new Error('memory
|
|
196
|
+
throw new Error('memory must be <scene>/<name> (copy that from the listing; the always-on scene is written global/<name>): ' + (memory || '(empty)'));
|
|
197
197
|
}
|
|
198
198
|
const group = memory.slice(0, slash);
|
|
199
199
|
const name = memory.slice(slash + 1);
|
|
@@ -201,7 +201,7 @@ export function buildMemoryTools(deps) {
|
|
|
201
201
|
// 这一侧 —— `rules-create` 对同名是**拒绝**的(`error.rules.exists`),op 不会 upsert。
|
|
202
202
|
const all = await deps.rulesOps['rules-list']({});
|
|
203
203
|
if (!all || all.ok === false)
|
|
204
|
-
throw new Error((all && all.error) || '
|
|
204
|
+
throw new Error((all && all.error) || 'failed to read memories');
|
|
205
205
|
const hit = (all.rules || []).find((x) => String(x.id) === memory);
|
|
206
206
|
if (hit) {
|
|
207
207
|
// 改的分支一律 read-modify-write(省略 = 保持),因为 `rules-update` 是整份重写。
|
|
@@ -209,7 +209,7 @@ export function buildMemoryTools(deps) {
|
|
|
209
209
|
// 模型以为改了点什么,其实只是把文件重写了一遍。
|
|
210
210
|
const touched = ['body', 'description', 'nextName'].filter((k) => args[k] !== undefined);
|
|
211
211
|
if (!touched.length)
|
|
212
|
-
throw new Error('
|
|
212
|
+
throw new Error('nothing to change: give at least one of body / description / nextName');
|
|
213
213
|
const payload = { id: memory };
|
|
214
214
|
// 只带模型真给过的键:`description` 传空串在 op 里是"删除并重新派生",与"未给"是两种
|
|
215
215
|
// 意图,都必须原样透传 —— 所以用 `!== undefined` 而不是真值判断。
|
|
@@ -221,10 +221,10 @@ export function buildMemoryTools(deps) {
|
|
|
221
221
|
payload.name = args.nextName;
|
|
222
222
|
const r = await deps.rulesOps['rules-update'](payload);
|
|
223
223
|
if (!r || r.ok === false)
|
|
224
|
-
throw new Error((r && r.error) || '
|
|
224
|
+
throw new Error((r && r.error) || 'failed to update the memory');
|
|
225
225
|
const rule = r.rule || {};
|
|
226
|
-
return 'OK: updated ' + String(rule.id || memory) + '
|
|
227
|
-
(rule.id && String(rule.id) !== memory ? ' →
|
|
226
|
+
return 'OK: updated ' + String(rule.id || memory) + ' (changed ' + touched.join(', ') + ')' +
|
|
227
|
+
(rule.id && String(rule.id) !== memory ? ' → now ' + rule.id : '');
|
|
228
228
|
}
|
|
229
229
|
// 建的分支:正文与描述都必填。`rules-create` 自己会拒空正文与非法名(ops/memory.ts),
|
|
230
230
|
// 但那是给用户看的中文 op 错;模型需要的是"这条工具要什么",所以在工具侧先挡、点名缺哪个。
|
|
@@ -235,7 +235,7 @@ export function buildMemoryTools(deps) {
|
|
|
235
235
|
description.trim() === '' ? 'description' : '',
|
|
236
236
|
].filter((x) => x !== '');
|
|
237
237
|
if (missing.length)
|
|
238
|
-
throw new Error('
|
|
238
|
+
throw new Error('creating a memory needs ' + missing.join(' and ') + ' (' + memory + ' does not exist yet)');
|
|
239
239
|
// 查重只**说出来**,不拒绝 —— 猜错一次就把该记的东西记不下去,比多记一条代价大得多;
|
|
240
240
|
// 而且"像不像同一条"本来就是人判断的事。只在建的分支提示:改的分支没有"再记一条"的风险。
|
|
241
241
|
const norm = (value) => String(value ?? '').toLowerCase().replace(/\s+/g, ' ').trim();
|
|
@@ -256,11 +256,11 @@ export function buildMemoryTools(deps) {
|
|
|
256
256
|
.slice(0, 3);
|
|
257
257
|
const r = await deps.rulesOps['rules-create']({ group, name, description, body });
|
|
258
258
|
if (!r || r.ok === false)
|
|
259
|
-
throw new Error((r && r.error) || '
|
|
259
|
+
throw new Error((r && r.error) || 'failed to create the memory');
|
|
260
260
|
// 刚建好的这条自己会在列表里,从名单里去掉。
|
|
261
261
|
const dup = similar.filter((id) => id !== String(r.rule.id));
|
|
262
262
|
const warn = dup.length
|
|
263
|
-
? `\
|
|
263
|
+
? `\nnote: ${dup.length} existing memories have a similar name or description (${dup.join(', ')}) — if it is the same thing, update that one instead of keeping two.`
|
|
264
264
|
: '';
|
|
265
265
|
// 回执必须同时说清**两个**条件(2026-10-15 smoke-test P1-3):新建的记忆在 op 层是
|
|
266
266
|
// `enabled: false`(ops/memory.ts 的 rulesCreate),而原先这句只提场景 —— 读起来像
|
|
@@ -268,10 +268,10 @@ export function buildMemoryTools(deps) {
|
|
|
268
268
|
// 的 enableHint 同一形状:点名的工具在出厂默认名单里是关着的,关着就改说"用户开启后"。
|
|
269
269
|
const sceneActive = group === 'global' ||
|
|
270
270
|
(group !== '' && (all.activeMode === 'all' || String(all.activeScene || '') === group));
|
|
271
|
-
const sceneTail = sceneActive ? '' : '
|
|
271
|
+
const sceneTail = sceneActive ? '' : ', and turn scene "' + group + '" on';
|
|
272
272
|
const enableHint = deps.toolVisible('memory_manager_switch')
|
|
273
|
-
? '
|
|
274
|
-
: '
|
|
273
|
+
? ' (created disabled — turn it on first with memory_manager_switch' + sceneTail + ')'
|
|
274
|
+
: ' (created disabled — the user has to turn it on first' + sceneTail + ')';
|
|
275
275
|
return 'OK: created ' + r.rule.id + enableHint + warn;
|
|
276
276
|
},
|
|
277
277
|
}));
|