dsh-tool-folder 0.2.3 → 0.2.4
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 +17 -0
- package/lib/index.js +25 -20
- package/lib/ux.js +107 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -149,6 +149,23 @@ were never unregistered, so nothing else changes.
|
|
|
149
149
|
|
|
150
150
|
## Changelog
|
|
151
151
|
|
|
152
|
+
### v0.2.4 — P1: batch tools_schema + sibling closure (2026-08-31)
|
|
153
|
+
1. **Batch expand** — `tools_schema` accepts `names: [...]` to expand several
|
|
154
|
+
tools in one call (`{ found, results: [...] }`); a single `name` keeps the
|
|
155
|
+
legacy 6-field shape byte-for-byte.
|
|
156
|
+
2. **Sibling closure** (`closureSize`, default 0 = off, mirrors the
|
|
157
|
+
`maxWarmTools` compat precedent): when enabled, expanding a tool also lists
|
|
158
|
+
lightweight sibling entries (name + 120-char description) from the same
|
|
159
|
+
server prefix, so the model can batch-expand what it actually needs —
|
|
160
|
+
kills the "expanded A, now I need B" round-trip. `closure: false` opts out
|
|
161
|
+
per call.
|
|
162
|
+
3. Output contract moved to a pure function (`buildSchemaResponse`, ux.js) —
|
|
163
|
+
11 new tests pin single/batch/missing/siblings branches (94 total green).
|
|
164
|
+
4. Two-axis audit (Standards + Spec): 1 Critical fixed during review — the
|
|
165
|
+
single/batch routing condition was inverted (old `name`-only calls would
|
|
166
|
+
have returned the batch shape); contract tests now catch that class
|
|
167
|
+
instantly. `siblingClosure` dead code removed.
|
|
168
|
+
|
|
152
169
|
### v0.2.3 — fold-ux: search fallback catalog + query expansion + stable results + discovery stats (2026-08-30)
|
|
153
170
|
1. **Zero-match fallback catalog** (`fallbackCatalogSize`, default 30): when
|
|
154
171
|
`tools_search` finds nothing, it returns a name-sorted lightweight catalog
|
package/lib/index.js
CHANGED
|
@@ -32,11 +32,11 @@ import fs from "node:fs";
|
|
|
32
32
|
import path from "node:path";
|
|
33
33
|
import z from "@deepseek-ai/schemastery";
|
|
34
34
|
import { buildIndex, search } from "./bm25.js";
|
|
35
|
-
import { compressTool, normalizeDescription
|
|
35
|
+
import { compressTool, normalizeDescription } from "./schema.js";
|
|
36
36
|
import { selectionCoverage, isHotTool, prioritizeExact, touchWarm, evictWarm, missingMetaTools } from "./metrics.js";
|
|
37
37
|
import { tierForBudget } from "./budget.js";
|
|
38
38
|
import { readState, writeState } from "./storage.js";
|
|
39
|
-
import { expandQuery, fallbackCatalog, createSearchCache, recordDiscoveries, serializeDiscoveries, deserializeDiscoveries } from "./ux.js";
|
|
39
|
+
import { expandQuery, fallbackCatalog, createSearchCache, recordDiscoveries, serializeDiscoveries, deserializeDiscoveries, siblingClosure, buildSchemaResponse } from "./ux.js";
|
|
40
40
|
import { ChainGuard, verdict } from "./chainguard.js";
|
|
41
41
|
import { matchCategory } from "./category.js";
|
|
42
42
|
import { toonifyValue } from "./toonify.js";
|
|
@@ -88,6 +88,8 @@ export const Config = z.object({
|
|
|
88
88
|
.description("P0: tools_search 零匹配时回退的轻目录条数(name+80字描述,名字典序);0=关闭回退(返回空=旧行为)"),
|
|
89
89
|
queryExpandEnabled: z.boolean().default(true)
|
|
90
90
|
.description("P0: 中文意图→英文同义词扩展(只加词不改原词,扩大 BM25/语义召回;精确名匹配不受影响)"),
|
|
91
|
+
closureSize: z.number().min(0).max(20).default(0)
|
|
92
|
+
.description("P1: tools_schema 展开时附带同 server 兄弟工具的轻目录条数(name+120字描述,名字典序);0=关闭(默认关=无配置升级输出逐字段等价,显式开启才启用,与 maxWarmTools 先例一致)"),
|
|
91
93
|
});
|
|
92
94
|
|
|
93
95
|
const DEFAULTS = {
|
|
@@ -126,6 +128,7 @@ const DEFAULTS = {
|
|
|
126
128
|
maxWarmTools: 0, // v0.2.2: warm LRU 上限(0=关闭,默认关=无配置升级逐字节等价)
|
|
127
129
|
fallbackCatalogSize: 30, // P0: 零匹配回退轻目录条数(0=关闭)
|
|
128
130
|
queryExpandEnabled: true, // P0: 中文→英文 query 扩展
|
|
131
|
+
closureSize: 0, // P1: tools_schema 同 server 兄弟闭包(默认关=逐字段等价,显式开启)
|
|
129
132
|
};
|
|
130
133
|
|
|
131
134
|
function feedbackPath(cfg) {
|
|
@@ -597,15 +600,24 @@ export function apply(ctx, config) {
|
|
|
597
600
|
const schemaTool = defineTool({
|
|
598
601
|
name: "tools_schema",
|
|
599
602
|
description:
|
|
600
|
-
"Get the complete parameter schema of one
|
|
603
|
+
"Get the complete parameter schema of one or more tools by name. Use after " +
|
|
601
604
|
"tools_search narrows candidates, or before calling a tool whose exact " +
|
|
602
605
|
"arguments you need. Returns the full JSON schema of parameters plus the " +
|
|
603
|
-
"full description
|
|
606
|
+
"full description; sibling tools from the same server are listed alongside " +
|
|
607
|
+
"(lightweight) so you can batch-expand what you need.",
|
|
604
608
|
parameters: {
|
|
605
609
|
name: {
|
|
606
610
|
type: "string",
|
|
607
|
-
|
|
608
|
-
|
|
611
|
+
description: "Exact tool name, e.g. 'mcp__viking__remember' (either name or names).",
|
|
612
|
+
},
|
|
613
|
+
names: {
|
|
614
|
+
type: "array",
|
|
615
|
+
items: { type: "string" },
|
|
616
|
+
description: "Batch: exact tool names to expand in one call (either name or names).",
|
|
617
|
+
},
|
|
618
|
+
closure: {
|
|
619
|
+
type: "boolean",
|
|
620
|
+
description: "Include lightweight sibling tools from the same server (default true).",
|
|
609
621
|
},
|
|
610
622
|
},
|
|
611
623
|
output: {
|
|
@@ -614,21 +626,14 @@ export function apply(ctx, config) {
|
|
|
614
626
|
},
|
|
615
627
|
isConcurrencySafe: () => true,
|
|
616
628
|
async execute(args) {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
629
|
+
// P1: 批量展开(names 数组)+ 同 server 兄弟闭包(closureSize 条轻目录)。
|
|
630
|
+
// 输出契约全部在 buildSchemaResponse(纯函数,lib/ux.js 可单测)。
|
|
631
|
+
const res = buildSchemaResponse(allTools, args, { closureSize: cfg.closureSize, serverOf });
|
|
620
632
|
// v0.2.2: 展开过 schema 的工具名进入 warm 集合,下一轮保持注入。
|
|
621
|
-
touchWarmNames(
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
server: serverOf(t.name),
|
|
626
|
-
description: t.description || "",
|
|
627
|
-
// C4 defense: even a schema containing `const: -0` must come back as
|
|
628
|
-
// lossless JSON.
|
|
629
|
-
parameters: sanitizeLossless(t.parameters ?? {}),
|
|
630
|
-
required: Array.isArray(t.parameters?.required) ? t.parameters.required : [],
|
|
631
|
-
};
|
|
633
|
+
touchWarmNames(
|
|
634
|
+
Array.isArray(res.results) ? res.results.map((r) => r.name) : res.found && res.name ? [res.name] : [],
|
|
635
|
+
);
|
|
636
|
+
return res;
|
|
632
637
|
},
|
|
633
638
|
});
|
|
634
639
|
try {
|
package/lib/ux.js
CHANGED
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
* 同会话重复搜索返回同一结果集(漂移的最小修)
|
|
11
11
|
* ④ recordDiscoveries 发现率统计(name→count,落盘可恢复)——低频工具名单
|
|
12
12
|
* 反哺描述改进(官方「监控发现率」建议的本地等价物)
|
|
13
|
+
* P1 ⑤ siblingClosure 同 server 兄弟闭包 ⑥ buildSchemaResponse tools_schema
|
|
14
|
+
* 输出契约(单数/批量/siblings/missing 全分支,可单测)
|
|
13
15
|
*/
|
|
16
|
+
import { sanitizeLossless } from "./schema.js";
|
|
14
17
|
|
|
15
18
|
/* ------------------------- ① query 扩展词表 ------------------------- */
|
|
16
19
|
|
|
@@ -176,3 +179,107 @@ export function deserializeDiscoveries(list) {
|
|
|
176
179
|
}
|
|
177
180
|
return m;
|
|
178
181
|
}
|
|
182
|
+
|
|
183
|
+
/* ------------------------- P1: 依赖闭包 ------------------------- */
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 同 server 兄弟闭包(graph-tool-call 的本地简化:server 前缀 = 依赖分组代理)。
|
|
187
|
+
* 展开目标工具时顺带返回同 server 的其它工具轻目录(name+120 字描述),
|
|
188
|
+
* 模型看到兄弟名字后可批量展开——减少「展开 A 发现还要 B」的往返税。
|
|
189
|
+
* ponytail: server 前缀是粗粒度依赖近似;精确 producer 图需要宿主工具元数据,
|
|
190
|
+
* 真需要时再按参数类型启发补。
|
|
191
|
+
* @param {Array<{name:string,description?:string}>} allTools
|
|
192
|
+
* @param {Array<string>} expandedNames 已展开的工具名
|
|
193
|
+
* @param {number} limit 附带兄弟总数上限(0=关闭)
|
|
194
|
+
* @param {(name:string)=>string} [serverOf] server 前缀提取
|
|
195
|
+
* @returns {Array<{name:string,description:string}>}
|
|
196
|
+
*/
|
|
197
|
+
export function siblingClosure(allTools, expandedNames, limit, serverOf) {
|
|
198
|
+
const lim = Math.max(0, Number(limit) || 0);
|
|
199
|
+
if (!lim || !Array.isArray(allTools) || !Array.isArray(expandedNames) || expandedNames.length === 0) return [];
|
|
200
|
+
const svc = typeof serverOf === "function" ? serverOf : (n) => String(n).split("__").slice(0, 2).join("__");
|
|
201
|
+
const servers = new Set(expandedNames.map((n) => svc(String(n))).filter(Boolean));
|
|
202
|
+
if (servers.size === 0) return [];
|
|
203
|
+
const skip = new Set(expandedNames);
|
|
204
|
+
const out = [];
|
|
205
|
+
for (const t of [...allTools].sort((a, b) => String(a.name).localeCompare(String(b.name)))) {
|
|
206
|
+
if (out.length >= lim) break;
|
|
207
|
+
const nm = String(t.name || "");
|
|
208
|
+
if (!nm || skip.has(nm)) continue;
|
|
209
|
+
if (servers.has(svc(nm))) {
|
|
210
|
+
out.push({ name: nm, description: String(t.description || "").slice(0, 120) });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* ------------------------- P1: tools_schema 输出契约 ------------------------- */
|
|
217
|
+
|
|
218
|
+
const defaultServerOf = (name) => String(name).split("__").slice(0, 2).join("__");
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* tools_schema 的纯输出构造(从 index.js execute 提取,契约可单测——single 判定
|
|
222
|
+
* 写反曾逃过 87 条纯函数测试,根因是 execute 无测试;提取后契约分支全部可钉死)。
|
|
223
|
+
* 单数调用(name 或 names 单元素,去重后 1 个)→ 旧版逐字段结构;
|
|
224
|
+
* 批量(去重后 >1 个)→ { found, results: [...], siblings?, missing? }。
|
|
225
|
+
* @param {Array<{name:string,description?:string,parameters?:object}>} allTools
|
|
226
|
+
* @param {{name?:string,names?:Array<string>,closure?:boolean}} args
|
|
227
|
+
* @param {{closureSize?:number,serverOf?:Function}} [opts]
|
|
228
|
+
*/
|
|
229
|
+
export function buildSchemaResponse(allTools, args, opts = {}) {
|
|
230
|
+
const closureSize = Math.max(0, Number(opts.closureSize) || 0);
|
|
231
|
+
const svc = typeof opts.serverOf === "function" ? opts.serverOf : defaultServerOf;
|
|
232
|
+
const primary = String(args?.name || "");
|
|
233
|
+
const batch = (Array.isArray(args?.names) ? args.names : []).map((n) => String(n)).filter(Boolean);
|
|
234
|
+
const want = [...new Set([...batch, ...(primary ? [primary] : [])])];
|
|
235
|
+
if (want.length === 0) return { found: false, reason: "no name" };
|
|
236
|
+
const expanded = [];
|
|
237
|
+
const missing = [];
|
|
238
|
+
for (const n of want) {
|
|
239
|
+
const t = (allTools || []).find((x) => x && x.name === n);
|
|
240
|
+
if (!t) {
|
|
241
|
+
missing.push(n);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
expanded.push({
|
|
245
|
+
name: t.name,
|
|
246
|
+
server: svc(t.name),
|
|
247
|
+
description: t.description || "",
|
|
248
|
+
// C4 defense: even a schema containing `const: -0` must come back as
|
|
249
|
+
// lossless JSON.
|
|
250
|
+
parameters: sanitizeLossless(t.parameters ?? {}),
|
|
251
|
+
required: Array.isArray(t.parameters?.required) ? t.parameters.required : [],
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
// ⚠️ 判定只看 want.length——旧实现用 !primary 导致只传 name 的调用误走批量结构。
|
|
255
|
+
const single = want.length === 1;
|
|
256
|
+
if (single) {
|
|
257
|
+
if (expanded.length === 0) return { name: want[0], found: false, reason: "unknown tool" };
|
|
258
|
+
const e = expanded[0];
|
|
259
|
+
const out = {
|
|
260
|
+
name: e.name,
|
|
261
|
+
found: true,
|
|
262
|
+
server: e.server,
|
|
263
|
+
description: e.description,
|
|
264
|
+
parameters: e.parameters,
|
|
265
|
+
required: e.required,
|
|
266
|
+
};
|
|
267
|
+
if (closureSize > 0 && args?.closure !== false) {
|
|
268
|
+
out.siblings = siblingClosure(allTools, [e.name], closureSize, svc);
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
const out = {
|
|
273
|
+
found: expanded.length > 0,
|
|
274
|
+
...(expanded.length > 0
|
|
275
|
+
? {
|
|
276
|
+
results: expanded,
|
|
277
|
+
...(closureSize > 0 && args?.closure !== false
|
|
278
|
+
? { siblings: siblingClosure(allTools, expanded.map((e) => e.name), closureSize, svc) }
|
|
279
|
+
: {}),
|
|
280
|
+
}
|
|
281
|
+
: {}),
|
|
282
|
+
...(missing.length > 0 ? { missing } : {}),
|
|
283
|
+
};
|
|
284
|
+
return out;
|
|
285
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tool-folder",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Fold the DSH tool surface per request + ChainGuard firewall (high-risk block + exfil-chain detection + anti-obfuscation) + BM25/bge-m3 hybrid tools_search. Shrinks schema tokens 80-90% while keeping selection accuracy. v0.2.0 adds a semantic retrieval leg (local Ollama bge-m3, RRF hybrid) and ChainGuard obfuscation detection (concat-rebuild / -enc / certutil / bitsadmin / homoglyphs).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh-plugin",
|