toolgz 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 +201 -0
- package/NOTICE +16 -0
- package/README.md +245 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +309 -0
- package/dist/providers/index.d.ts +69 -0
- package/dist/providers/index.js +108 -0
- package/dist/recommend.d.ts +41 -0
- package/dist/recommend.js +56 -0
- package/dist/render/index.d.ts +34 -0
- package/dist/render/index.js +143 -0
- package/dist/runtime/similar.d.ts +26 -0
- package/dist/runtime/similar.js +86 -0
- package/dist/runtime/validate.d.ts +11 -0
- package/dist/runtime/validate.js +87 -0
- package/dist/types.d.ts +112 -0
- package/dist/types.js +2 -0
- package/package.json +71 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/** JSON Schema keys that carry no information the sampler can act on. */
|
|
2
|
+
const DROP_KEYS = new Set([
|
|
3
|
+
"description",
|
|
4
|
+
"$schema",
|
|
5
|
+
"title",
|
|
6
|
+
"examples",
|
|
7
|
+
"default",
|
|
8
|
+
"additionalProperties",
|
|
9
|
+
"$id",
|
|
10
|
+
"$comment",
|
|
11
|
+
]);
|
|
12
|
+
/**
|
|
13
|
+
* Strip prose and boilerplate from a JSON Schema while preserving everything
|
|
14
|
+
* that constrains decoding: types, enums, required, item types, nesting.
|
|
15
|
+
*
|
|
16
|
+
* This is the single highest-leverage transform in the library — per-property
|
|
17
|
+
* `description` strings are the bulk of a real MCP tool definition.
|
|
18
|
+
*/
|
|
19
|
+
export function flattenSchema(schema) {
|
|
20
|
+
if (!schema || typeof schema !== "object")
|
|
21
|
+
return { type: "object" };
|
|
22
|
+
const out = {};
|
|
23
|
+
for (const [k, v] of Object.entries(schema)) {
|
|
24
|
+
if (DROP_KEYS.has(k))
|
|
25
|
+
continue;
|
|
26
|
+
if (k === "properties" && v && typeof v === "object") {
|
|
27
|
+
out.properties = Object.fromEntries(Object.entries(v).map(([pk, pv]) => [
|
|
28
|
+
pk,
|
|
29
|
+
flattenProperty(pv),
|
|
30
|
+
]));
|
|
31
|
+
}
|
|
32
|
+
else if (k === "items") {
|
|
33
|
+
out.items = flattenProperty(v);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
out[k] = v;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (!out.type)
|
|
40
|
+
out.type = "object";
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
function flattenProperty(p) {
|
|
44
|
+
if (!p || typeof p !== "object")
|
|
45
|
+
return p;
|
|
46
|
+
if (Array.isArray(p))
|
|
47
|
+
return p.map(flattenProperty);
|
|
48
|
+
const out = {};
|
|
49
|
+
for (const [k, v] of Object.entries(p)) {
|
|
50
|
+
if (DROP_KEYS.has(k))
|
|
51
|
+
continue;
|
|
52
|
+
if (k === "properties" && v && typeof v === "object") {
|
|
53
|
+
out.properties = Object.fromEntries(Object.entries(v).map(([pk, pv]) => [
|
|
54
|
+
pk,
|
|
55
|
+
flattenProperty(pv),
|
|
56
|
+
]));
|
|
57
|
+
}
|
|
58
|
+
else if (k === "items") {
|
|
59
|
+
out.items = flattenProperty(v);
|
|
60
|
+
}
|
|
61
|
+
else if (k === "anyOf" || k === "oneOf" || k === "allOf") {
|
|
62
|
+
out[k] = v.map(flattenProperty);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
out[k] = v;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* `name(required, optional?:a|b, list?:string[])`
|
|
72
|
+
*
|
|
73
|
+
* A model reads this natively. A 400-token JSON Schema becomes one line.
|
|
74
|
+
*/
|
|
75
|
+
export function signatureLine(tool, nameOverride) {
|
|
76
|
+
const schema = tool.schema ??
|
|
77
|
+
tool.inputSchema ??
|
|
78
|
+
tool.input_schema ??
|
|
79
|
+
{};
|
|
80
|
+
const props = schema.properties ?? {};
|
|
81
|
+
const required = new Set(schema.required ?? []);
|
|
82
|
+
const parts = Object.entries(props).map(([key, raw]) => {
|
|
83
|
+
const v = raw;
|
|
84
|
+
const opt = required.has(key) ? "" : "?";
|
|
85
|
+
let hint = "";
|
|
86
|
+
if (v?.enum)
|
|
87
|
+
hint = `:${v.enum.join("|")}`;
|
|
88
|
+
else if (v?.type === "array" && v.items?.type)
|
|
89
|
+
hint = `:${v.items.type}[]`;
|
|
90
|
+
return `${key}${opt}${hint}`;
|
|
91
|
+
});
|
|
92
|
+
return `${nameOverride ?? tool.name}(${parts.join(",")})`;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Aggressively shortened descriptor: first sentence, stop-words dropped,
|
|
96
|
+
* lowercased. Used by the `terse` map style, which trades legibility (and the
|
|
97
|
+
* real tool name) for a few tokens per line.
|
|
98
|
+
*/
|
|
99
|
+
export function terseDescriptor(s) {
|
|
100
|
+
return firstSentence(s)
|
|
101
|
+
.replace(/\b(the|a|an|to|of|for|in|with|and|that|this)\b ?/gi, "")
|
|
102
|
+
.replace(/[.!?]+$/, "")
|
|
103
|
+
.replace(/\s+/g, " ")
|
|
104
|
+
.trim()
|
|
105
|
+
.toLowerCase();
|
|
106
|
+
}
|
|
107
|
+
/** First sentence only — the rest is almost always restatement. */
|
|
108
|
+
export function firstSentence(s) {
|
|
109
|
+
const m = s.match(/^(.*?[.!?])(\s|$)/s);
|
|
110
|
+
return (m ? m[1] : s).trim();
|
|
111
|
+
}
|
|
112
|
+
/** Cheap size proxy for stats. Not a token count — never used for billing. */
|
|
113
|
+
export function countSchemaTokensApprox(x) {
|
|
114
|
+
return JSON.stringify(x ?? "").length;
|
|
115
|
+
}
|
|
116
|
+
/** Default namespace split: `github_create_issue` → github / create_issue. */
|
|
117
|
+
export function defaultNamespaceOf(name) {
|
|
118
|
+
const i = name.search(/[_.]/);
|
|
119
|
+
if (i === -1)
|
|
120
|
+
return { ns: name, op: name };
|
|
121
|
+
return { ns: name.slice(0, i), op: name.slice(i + 1) };
|
|
122
|
+
}
|
|
123
|
+
export function normalize(tools, namespaceOf) {
|
|
124
|
+
const seen = new Set();
|
|
125
|
+
const out = tools.map((t) => {
|
|
126
|
+
if (!t?.name)
|
|
127
|
+
throw new Error("tool is missing a name");
|
|
128
|
+
if (seen.has(t.name))
|
|
129
|
+
throw new Error(`duplicate tool name: ${t.name}`);
|
|
130
|
+
seen.add(t.name);
|
|
131
|
+
const { ns, op } = namespaceOf(t.name);
|
|
132
|
+
return {
|
|
133
|
+
name: t.name,
|
|
134
|
+
description: t.description ?? "",
|
|
135
|
+
schema: t.inputSchema ?? t.input_schema ?? { type: "object" },
|
|
136
|
+
ns,
|
|
137
|
+
op,
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
// Deterministic ordering. Prefix stability is what makes prompt caching work;
|
|
141
|
+
// a caller iterating a Map or Set must not silently change the cache key.
|
|
142
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
143
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nearest-name matching for argument-error messages.
|
|
3
|
+
*
|
|
4
|
+
* Motivation, from measurement: across 360 benchmark runs the single most
|
|
5
|
+
* common failure was a model passing `query` to a parameter named `q` — 14 of
|
|
6
|
+
* 18 rejections. Each one burned a turn, and on a reasoning model a wasted turn
|
|
7
|
+
* is a wasted round of thinking, so this is a cost problem as much as a
|
|
8
|
+
* correctness one.
|
|
9
|
+
*
|
|
10
|
+
* We deliberately do NOT auto-remap. Silently moving a value from `query` to
|
|
11
|
+
* `q` would be guessing about caller intent, and a wrong guess dispatches bad
|
|
12
|
+
* data instead of raising. Instead we name the likely fix so the retry lands
|
|
13
|
+
* first time.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Similarity in 0–100. Tuned so the cases we actually observed score high:
|
|
17
|
+
* `q`↔`query` (prefix), `pageSize`↔`per_page` (shared word), and unrelated
|
|
18
|
+
* names stay low enough to be filtered out.
|
|
19
|
+
*/
|
|
20
|
+
export declare function similarity(a: string, b: string): number;
|
|
21
|
+
/**
|
|
22
|
+
* Closest candidate to `name`, or null when nothing is close enough to be worth
|
|
23
|
+
* suggesting. The floor matters: a bad suggestion is worse than none, because
|
|
24
|
+
* the model will act on it.
|
|
25
|
+
*/
|
|
26
|
+
export declare function nearest(name: string, candidates: readonly string[], floor?: number): string | null;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nearest-name matching for argument-error messages.
|
|
3
|
+
*
|
|
4
|
+
* Motivation, from measurement: across 360 benchmark runs the single most
|
|
5
|
+
* common failure was a model passing `query` to a parameter named `q` — 14 of
|
|
6
|
+
* 18 rejections. Each one burned a turn, and on a reasoning model a wasted turn
|
|
7
|
+
* is a wasted round of thinking, so this is a cost problem as much as a
|
|
8
|
+
* correctness one.
|
|
9
|
+
*
|
|
10
|
+
* We deliberately do NOT auto-remap. Silently moving a value from `query` to
|
|
11
|
+
* `q` would be guessing about caller intent, and a wrong guess dispatches bad
|
|
12
|
+
* data instead of raising. Instead we name the likely fix so the retry lands
|
|
13
|
+
* first time.
|
|
14
|
+
*/
|
|
15
|
+
/** Levenshtein distance, iterative single-row. */
|
|
16
|
+
function editDistance(a, b) {
|
|
17
|
+
if (a === b)
|
|
18
|
+
return 0;
|
|
19
|
+
if (!a.length)
|
|
20
|
+
return b.length;
|
|
21
|
+
if (!b.length)
|
|
22
|
+
return a.length;
|
|
23
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
24
|
+
for (let i = 1; i <= a.length; i++) {
|
|
25
|
+
const row = [i];
|
|
26
|
+
for (let j = 1; j <= b.length; j++) {
|
|
27
|
+
row[j] = Math.min(prev[j] + 1, row[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
28
|
+
}
|
|
29
|
+
prev = row;
|
|
30
|
+
}
|
|
31
|
+
return prev[b.length];
|
|
32
|
+
}
|
|
33
|
+
/** `per_page` / `perPage` / `PerPage` all normalise to `perpage`. */
|
|
34
|
+
const norm = (s) => s.toLowerCase().replace(/[_\-\s]/g, "");
|
|
35
|
+
/** Split camelCase and snake_case into lowercase word parts. */
|
|
36
|
+
const words = (s) => s
|
|
37
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
38
|
+
.split(/[_\-\s]+/)
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.map((w) => w.toLowerCase());
|
|
41
|
+
/**
|
|
42
|
+
* Similarity in 0–100. Tuned so the cases we actually observed score high:
|
|
43
|
+
* `q`↔`query` (prefix), `pageSize`↔`per_page` (shared word), and unrelated
|
|
44
|
+
* names stay low enough to be filtered out.
|
|
45
|
+
*/
|
|
46
|
+
export function similarity(a, b) {
|
|
47
|
+
if (!a || !b)
|
|
48
|
+
return 0;
|
|
49
|
+
const na = norm(a);
|
|
50
|
+
const nb = norm(b);
|
|
51
|
+
if (na === nb)
|
|
52
|
+
return 100;
|
|
53
|
+
// A short name that prefixes a longer one is the `q`/`query` case.
|
|
54
|
+
const [short, long] = na.length <= nb.length ? [na, nb] : [nb, na];
|
|
55
|
+
if (long.startsWith(short) || long.endsWith(short))
|
|
56
|
+
return 85;
|
|
57
|
+
const wa = new Set(words(a));
|
|
58
|
+
const wb = new Set(words(b));
|
|
59
|
+
const shared = [...wa].filter((w) => wb.has(w) && w.length > 2).length;
|
|
60
|
+
if (shared) {
|
|
61
|
+
const union = new Set([...wa, ...wb]).size;
|
|
62
|
+
return 50 + Math.round((shared / union) * 30);
|
|
63
|
+
}
|
|
64
|
+
if (long.includes(short) && short.length >= 3)
|
|
65
|
+
return 60;
|
|
66
|
+
const dist = editDistance(na, nb);
|
|
67
|
+
const ratio = 1 - dist / Math.max(na.length, nb.length);
|
|
68
|
+
return Math.max(0, Math.round(ratio * 55));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Closest candidate to `name`, or null when nothing is close enough to be worth
|
|
72
|
+
* suggesting. The floor matters: a bad suggestion is worse than none, because
|
|
73
|
+
* the model will act on it.
|
|
74
|
+
*/
|
|
75
|
+
export function nearest(name, candidates, floor = 55) {
|
|
76
|
+
let best = null;
|
|
77
|
+
let bestScore = 0;
|
|
78
|
+
for (const c of candidates) {
|
|
79
|
+
const s = similarity(name, c);
|
|
80
|
+
if (s > bestScore) {
|
|
81
|
+
bestScore = s;
|
|
82
|
+
best = c;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return bestScore >= floor ? best : null;
|
|
86
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { NormalizedTool } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Validate arguments against the *original* schema before dispatch.
|
|
4
|
+
*
|
|
5
|
+
* This matters most at levels 2 and 3: routing calls through a generic
|
|
6
|
+
* dispatcher moves argument checking out of the provider's constrained
|
|
7
|
+
* sampler and into here. Error strings are written for the model to read —
|
|
8
|
+
* they name the tool, the offending parameter, and where possible the exact
|
|
9
|
+
* fix, because a vague error costs another turn and another round of thinking.
|
|
10
|
+
*/
|
|
11
|
+
export declare function validateArgs(tool: NormalizedTool, args: Record<string, any>): string | null;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { nearest } from "./similar.js";
|
|
2
|
+
/**
|
|
3
|
+
* Validate arguments against the *original* schema before dispatch.
|
|
4
|
+
*
|
|
5
|
+
* This matters most at levels 2 and 3: routing calls through a generic
|
|
6
|
+
* dispatcher moves argument checking out of the provider's constrained
|
|
7
|
+
* sampler and into here. Error strings are written for the model to read —
|
|
8
|
+
* they name the tool, the offending parameter, and where possible the exact
|
|
9
|
+
* fix, because a vague error costs another turn and another round of thinking.
|
|
10
|
+
*/
|
|
11
|
+
export function validateArgs(tool, args) {
|
|
12
|
+
const schema = tool.schema ?? {};
|
|
13
|
+
const props = schema.properties ?? {};
|
|
14
|
+
const required = schema.required ?? [];
|
|
15
|
+
const known = Object.keys(props);
|
|
16
|
+
const supplied = Object.keys(args);
|
|
17
|
+
// Keys the caller sent that the schema does not define. Computed first so a
|
|
18
|
+
// missing-required error can point at the likely rename.
|
|
19
|
+
const unknown = known.length ? supplied.filter((k) => !props[k]) : [];
|
|
20
|
+
for (const key of required) {
|
|
21
|
+
if (args[key] === undefined || args[key] === null || args[key] === "") {
|
|
22
|
+
// The `query` → `q` case: a required key is absent and an undefined key
|
|
23
|
+
// was supplied that looks like it. Suggest, never silently remap.
|
|
24
|
+
const guess = nearest(key, unknown);
|
|
25
|
+
const hint = guess
|
|
26
|
+
? ` You passed "${guess}" — did you mean "${key}"? Rename it.`
|
|
27
|
+
: "";
|
|
28
|
+
return (`Missing required parameter "${key}" for ${tool.name}.` +
|
|
29
|
+
` Required: ${required.join(", ")}.${hint}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (unknown.length) {
|
|
33
|
+
const key = unknown[0];
|
|
34
|
+
const guess = nearest(key, known);
|
|
35
|
+
const hint = guess ? ` Did you mean "${guess}"?` : "";
|
|
36
|
+
return (`Unknown parameter "${key}" for ${tool.name}.` +
|
|
37
|
+
` Accepted: ${known.join(", ")}.${hint}`);
|
|
38
|
+
}
|
|
39
|
+
for (const [key, value] of Object.entries(args)) {
|
|
40
|
+
const spec = props[key];
|
|
41
|
+
if (!spec)
|
|
42
|
+
continue;
|
|
43
|
+
const problem = checkType(tool.name, key, spec, value);
|
|
44
|
+
if (problem)
|
|
45
|
+
return problem;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
function checkType(toolName, key, spec, value) {
|
|
50
|
+
if (spec.enum && !spec.enum.includes(value)) {
|
|
51
|
+
// Case drift ("approve" for "APPROVE") is common; show the exact spelling
|
|
52
|
+
// rather than only the list, so the retry is mechanical.
|
|
53
|
+
const ci = typeof value === "string"
|
|
54
|
+
? spec.enum.find((e) => typeof e === "string" && e.toLowerCase() === value.toLowerCase())
|
|
55
|
+
: undefined;
|
|
56
|
+
const hint = ci ? ` Use exactly "${ci}".` : "";
|
|
57
|
+
return (`Invalid value for "${key}" on ${toolName}:` +
|
|
58
|
+
` expected one of ${spec.enum.join(", ")}.${hint}`);
|
|
59
|
+
}
|
|
60
|
+
switch (spec.type) {
|
|
61
|
+
case "string":
|
|
62
|
+
if (typeof value !== "string")
|
|
63
|
+
return `Parameter "${key}" on ${toolName} must be a string.`;
|
|
64
|
+
break;
|
|
65
|
+
case "integer":
|
|
66
|
+
if (!Number.isInteger(value))
|
|
67
|
+
return `Parameter "${key}" on ${toolName} must be an integer.`;
|
|
68
|
+
break;
|
|
69
|
+
case "number":
|
|
70
|
+
if (typeof value !== "number")
|
|
71
|
+
return `Parameter "${key}" on ${toolName} must be a number.`;
|
|
72
|
+
break;
|
|
73
|
+
case "boolean":
|
|
74
|
+
if (typeof value !== "boolean")
|
|
75
|
+
return `Parameter "${key}" on ${toolName} must be a boolean.`;
|
|
76
|
+
break;
|
|
77
|
+
case "array":
|
|
78
|
+
if (!Array.isArray(value))
|
|
79
|
+
return `Parameter "${key}" on ${toolName} must be an array.`;
|
|
80
|
+
break;
|
|
81
|
+
case "object":
|
|
82
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
83
|
+
return `Parameter "${key}" on ${toolName} must be an object.`;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/** Public types. Everything a consumer touches is declared here. */
|
|
2
|
+
export type JsonSchema = {
|
|
3
|
+
type?: string;
|
|
4
|
+
properties?: Record<string, any>;
|
|
5
|
+
required?: string[];
|
|
6
|
+
items?: any;
|
|
7
|
+
enum?: any[];
|
|
8
|
+
[k: string]: any;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* A tool as you already have it — the same shape MCP servers and every major
|
|
12
|
+
* SDK produce. `inputSchema` and `input_schema` are both accepted.
|
|
13
|
+
*/
|
|
14
|
+
export type Tool = {
|
|
15
|
+
name: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
inputSchema?: JsonSchema;
|
|
18
|
+
input_schema?: JsonSchema;
|
|
19
|
+
};
|
|
20
|
+
/** Normalized internal form. */
|
|
21
|
+
export type NormalizedTool = {
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
schema: JsonSchema;
|
|
25
|
+
ns: string;
|
|
26
|
+
op: string;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Compression levels. Each is a superset of the previous.
|
|
30
|
+
*
|
|
31
|
+
* 0 passthrough — no change; useful as an A/B control in your own app
|
|
32
|
+
* 1 signature — flatten JSON Schema, keep native tools + real names
|
|
33
|
+
* 2 namespace — collapse related ops into one tool per namespace
|
|
34
|
+
* 3 minified — single dispatcher + opaque codes
|
|
35
|
+
*/
|
|
36
|
+
export type Level = 0 | 1 | 2 | 3;
|
|
37
|
+
/**
|
|
38
|
+
* How each line of the level-3 `<toolmap>` is rendered.
|
|
39
|
+
*
|
|
40
|
+
* name `a0 github_create_issue`
|
|
41
|
+
* name+required `a0 github_create_issue owner,repo,title`
|
|
42
|
+
* signature `a0 github_create_issue(owner,repo,title,body?,labels?)`
|
|
43
|
+
* terse `a0 create new issue in repository`
|
|
44
|
+
*
|
|
45
|
+
* `name` is the default and the smallest. `name+required` costs a few tokens
|
|
46
|
+
* per tool and exists to cut malformed arguments on models that fill the
|
|
47
|
+
* generic argument bag badly — the dispatcher levels give up provider-side
|
|
48
|
+
* constrained decoding, and this buys some of it back cheaply. `signature` also
|
|
49
|
+
* names the optional parameters, which removes most remaining `q()` lookups —
|
|
50
|
+
* a bigger cached map traded for fewer turns, which matters on models where
|
|
51
|
+
* every turn pays for a fresh round of reasoning. `terse` drops the real name
|
|
52
|
+
* entirely; it is the most aggressive and the least legible.
|
|
53
|
+
*/
|
|
54
|
+
export type MapStyle = "name" | "name+required" | "signature" | "terse";
|
|
55
|
+
export type CompressOptions = {
|
|
56
|
+
level?: Level;
|
|
57
|
+
/** Level 3 only. Ignored at levels 0–2, which emit no map. Default "name". */
|
|
58
|
+
mapStyle?: MapStyle;
|
|
59
|
+
/**
|
|
60
|
+
* Group tools into namespaces. Default splits on the first `_` or `.`,
|
|
61
|
+
* which matches MCP naming convention (`github_create_issue`).
|
|
62
|
+
*/
|
|
63
|
+
namespaceOf?: (toolName: string) => {
|
|
64
|
+
ns: string;
|
|
65
|
+
op: string;
|
|
66
|
+
};
|
|
67
|
+
/** Override the short alias used for a namespace at level 2. */
|
|
68
|
+
aliasOf?: (ns: string) => string;
|
|
69
|
+
/** Cap how many results a search/query meta-call returns. Default 8. */
|
|
70
|
+
searchLimit?: number;
|
|
71
|
+
/** Validate arguments against the original schema before dispatch. Default true. */
|
|
72
|
+
validate?: boolean;
|
|
73
|
+
};
|
|
74
|
+
export type Resolution = {
|
|
75
|
+
kind: "call";
|
|
76
|
+
name: string;
|
|
77
|
+
args: Record<string, any>;
|
|
78
|
+
} | {
|
|
79
|
+
kind: "meta";
|
|
80
|
+
name: string;
|
|
81
|
+
result: string;
|
|
82
|
+
} | {
|
|
83
|
+
kind: "error";
|
|
84
|
+
message: string;
|
|
85
|
+
recoverable: boolean;
|
|
86
|
+
};
|
|
87
|
+
export type CompressStats = {
|
|
88
|
+
level: Level;
|
|
89
|
+
toolCount: number;
|
|
90
|
+
wireToolCount: number;
|
|
91
|
+
originalChars: number;
|
|
92
|
+
compressedChars: number;
|
|
93
|
+
savedPct: number;
|
|
94
|
+
};
|
|
95
|
+
export type CompressResult = {
|
|
96
|
+
/** Tool definitions to send on the wire, in Anthropic shape. */
|
|
97
|
+
tools: unknown[];
|
|
98
|
+
/** Text to append to your system prompt. Empty string at levels 0–1. */
|
|
99
|
+
systemPreamble: string;
|
|
100
|
+
/** Whether the preamble should sit behind a cache breakpoint. */
|
|
101
|
+
cachePreamble: boolean;
|
|
102
|
+
/** Translate a raw model tool call back to a real one. */
|
|
103
|
+
resolve(rawName: string, rawArgs: Record<string, any>): Resolution;
|
|
104
|
+
/** Map a real tool name to its level-3 code. Throws below level 3. */
|
|
105
|
+
codeFor(toolName: string): string;
|
|
106
|
+
/** Build the raw call a model would emit for a real tool. Test/debug aid. */
|
|
107
|
+
encodeCallForTest(toolName: string, args: Record<string, any>): {
|
|
108
|
+
name: string;
|
|
109
|
+
args: Record<string, any>;
|
|
110
|
+
};
|
|
111
|
+
stats: CompressStats;
|
|
112
|
+
};
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "toolgz",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Compress LLM tool definitions to reclaim context window. Measured across Anthropic, OpenAI, Google and xAI.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "vitest run --exclude '**/integration/**'",
|
|
8
|
+
"brain": "tsx --disable-warning=ExperimentalWarning brain/brain.ts",
|
|
9
|
+
"bench": "tsx bench/harness/run.ts",
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"prepublishOnly": "npm run build && npm test",
|
|
12
|
+
"test:live": "vitest run tests/integration"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"llm",
|
|
16
|
+
"mcp",
|
|
17
|
+
"tools",
|
|
18
|
+
"tool-calling",
|
|
19
|
+
"context-window",
|
|
20
|
+
"tokens",
|
|
21
|
+
"anthropic",
|
|
22
|
+
"claude",
|
|
23
|
+
"openai",
|
|
24
|
+
"gemini",
|
|
25
|
+
"grok",
|
|
26
|
+
"prompt-caching"
|
|
27
|
+
],
|
|
28
|
+
"author": "",
|
|
29
|
+
"license": "Apache-2.0",
|
|
30
|
+
"type": "module",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@anthropic-ai/sdk": "^0.115.0",
|
|
33
|
+
"@google/genai": "^2.13.0",
|
|
34
|
+
"dotenv": "^17.4.2",
|
|
35
|
+
"openai": "^6.49.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^26.1.1",
|
|
39
|
+
"tsx": "^4.23.1",
|
|
40
|
+
"typescript": "^7.0.2",
|
|
41
|
+
"vitest": "^4.1.10"
|
|
42
|
+
},
|
|
43
|
+
"types": "dist/index.d.ts",
|
|
44
|
+
"files": [
|
|
45
|
+
"dist",
|
|
46
|
+
"README.md",
|
|
47
|
+
"LICENSE",
|
|
48
|
+
"NOTICE"
|
|
49
|
+
],
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=20"
|
|
52
|
+
},
|
|
53
|
+
"exports": {
|
|
54
|
+
".": {
|
|
55
|
+
"types": "./dist/index.d.ts",
|
|
56
|
+
"import": "./dist/index.js"
|
|
57
|
+
},
|
|
58
|
+
"./providers": {
|
|
59
|
+
"types": "./dist/providers/index.d.ts",
|
|
60
|
+
"import": "./dist/providers/index.js"
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"repository": {
|
|
64
|
+
"type": "git",
|
|
65
|
+
"url": "git+https://github.com/dperussina/toolgz.git"
|
|
66
|
+
},
|
|
67
|
+
"homepage": "https://github.com/dperussina/toolgz#readme",
|
|
68
|
+
"bugs": {
|
|
69
|
+
"url": "https://github.com/dperussina/toolgz/issues"
|
|
70
|
+
}
|
|
71
|
+
}
|