@stll/folio-cli 0.3.4 → 0.4.1

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.
@@ -1,3 +1,4 @@
1
+ import { expandBatchOperations } from "./batch-operations.js";
1
2
  import { checkExpectedVersion, openReviewer, readDocumentFile } from "./document.js";
2
3
  import { acquireLeaseForWrite } from "./editor-lease.js";
3
4
  import { FOLIO_CLI_ERROR_CODES, cliError } from "./errors.js";
@@ -8,10 +9,10 @@ import { diffPackages, nextRevisionIdSeed } from "./package-parts.js";
8
9
  import { SIDECAR_DIRECTORY, inspectPath, readSidecarFile, unsafePath } from "./sidecar.js";
9
10
  import { commitTransaction } from "./transaction.js";
10
11
  import { Result, panic } from "better-result";
11
- import { createReviewerBridge } from "@stll/folio-agents/bridges/reviewer";
12
- import { generateRedlineDocx } from "@stll/folio-agents/compare";
13
12
  import { executeFolioToolCallUntyped } from "@stll/folio-agents/execute";
14
13
  import { FOLIO_AGENT_TOOL_NAMES } from "@stll/folio-agents/types";
14
+ import { createReviewerBridge } from "@stll/folio-agents/bridges/reviewer";
15
+ import { generateRedlineDocx } from "@stll/folio-agents/compare";
15
16
  import { realpath, stat } from "node:fs/promises";
16
17
  import path from "node:path";
17
18
  import { createHash, randomUUID } from "node:crypto";
@@ -110,32 +111,50 @@ const saveReviewer = async (reviewer, repack) => {
110
111
  const commentExists = (reviewer, commentId) => reviewer.getComments().some((thread) => String(thread.id) === commentId || thread.replies.some((reply) => String(reply.id) === commentId));
111
112
  const mutateWithAgentTool = async ({ tool, source, args, options }) => {
112
113
  if (tool.type !== "agentWrite") return panic("mutateWithAgentTool needs an agentWrite tool");
113
- const reviewer = await openReviewer(source, options.author);
114
- if (reviewer.isErr()) return Result.err(reviewer.error);
114
+ const opened = await openReviewer(source, options.author);
115
+ if (opened.isErr()) return Result.err(opened.error);
116
+ const { reviewer } = opened.value;
115
117
  const idSeed = await nextRevisionIdSeed(source.bytes);
116
118
  if (idSeed.isErr()) return Result.err(idSeed.error);
117
- if ((tool.agentTool === FOLIO_AGENT_TOOL_NAMES.replyComment || tool.agentTool === FOLIO_AGENT_TOOL_NAMES.resolveComment) && !commentExists(reviewer.value, args["commentId"])) return Result.err(cliError({
119
+ if ((tool.agentTool === FOLIO_AGENT_TOOL_NAMES.replyComment || tool.agentTool === FOLIO_AGENT_TOOL_NAMES.resolveComment) && !commentExists(reviewer, args["commentId"])) return Result.err(cliError({
118
120
  code: FOLIO_CLI_ERROR_CODES.notFound,
119
121
  message: `No comment with id ${JSON.stringify(args["commentId"])}.`,
120
122
  hint: "Take the id from folio comments."
121
123
  }));
122
- const bridge = createReviewerBridge(reviewer.value, {
124
+ const bridge = createReviewerBridge(reviewer, {
123
125
  mode: tool.editMode === "tracked-or-direct" ? options.mode : "tracked-changes",
124
126
  revisionStamp: {
125
127
  date: options.date,
126
128
  idSeed: idSeed.value
127
129
  }
128
130
  });
129
- const executed = executeFolioToolCallUntyped(tool.agentTool, args, bridge);
131
+ let callArgs = args;
132
+ let callOptions = {};
133
+ let replaced = [];
134
+ if (tool.agentTool === FOLIO_AGENT_TOOL_NAMES.suggestChanges) {
135
+ const batch = expandBatchOperations(args["operations"], bridge);
136
+ if (batch.isErr()) return Result.err(batch.error);
137
+ callArgs = {
138
+ ...args,
139
+ operations: batch.value.operations
140
+ };
141
+ callOptions = batch.value.options;
142
+ replaced = batch.value.replaced;
143
+ }
144
+ const executed = executeFolioToolCallUntyped(tool.agentTool, callArgs, bridge, callOptions);
130
145
  if (!executed.ok) return Result.err(invalidInput(executed.error));
131
146
  const refusal = refusalFor(executed.result);
132
147
  if (refusal !== null) return Result.err(refusal);
133
- const saved = await saveReviewer(reviewer.value, options.repack);
148
+ const saved = await saveReviewer(reviewer, options.repack);
134
149
  if (saved.isErr()) return Result.err(saved.error);
135
150
  const receipts = isRecord(executed.result) && Array.isArray(executed.result["receipts"]) ? executed.result["receipts"] : [];
151
+ const result = replaced.length > 0 && isRecord(executed.result) ? {
152
+ ...executed.result,
153
+ replaced
154
+ } : executed.result;
136
155
  return Result.ok({
137
156
  ...saved.value,
138
- result: executed.result,
157
+ result,
139
158
  receipts
140
159
  });
141
160
  };
@@ -145,11 +164,12 @@ const resolveChanges = async ({ source, args, options }) => {
145
164
  if (!isResolveAction(action)) return Result.err(invalidInput("resolve_changes needs action \"accept\" or \"reject\"."));
146
165
  const idList = Array.isArray(ids) && ids.every((id) => typeof id === "string") ? ids : void 0;
147
166
  if ((idList !== void 0 && idList.length > 0) === (all === true)) return Result.err(invalidInput("Pass change ids or all, exactly one of them.", "folio accept --id 12 or --all."));
148
- const reviewer = await openReviewer(source, options.author);
149
- if (reviewer.isErr()) return Result.err(reviewer.error);
167
+ const opened = await openReviewer(source, options.author);
168
+ if (opened.isErr()) return Result.err(opened.error);
169
+ const { reviewer } = opened.value;
150
170
  let resolved;
151
171
  if (all === true) {
152
- const count = action === "accept" ? reviewer.value.acceptAll() : reviewer.value.rejectAll();
172
+ const count = action === "accept" ? reviewer.acceptAll() : reviewer.rejectAll();
153
173
  if (count === 0) return Result.err(cliError({
154
174
  code: FOLIO_CLI_ERROR_CODES.operationRejected,
155
175
  message: "The document has no pending tracked changes; nothing was written."
@@ -157,7 +177,7 @@ const resolveChanges = async ({ source, args, options }) => {
157
177
  resolved = count;
158
178
  } else {
159
179
  const selected = idList ?? [];
160
- const known = new Set(reviewer.value.getChanges().map(({ id }) => String(id)));
180
+ const known = new Set(reviewer.getChanges().map(({ id }) => String(id)));
161
181
  const missing = selected.filter((id) => !known.has(id));
162
182
  if (missing.length > 0) return Result.err(cliError({
163
183
  code: FOLIO_CLI_ERROR_CODES.notFound,
@@ -165,20 +185,20 @@ const resolveChanges = async ({ source, args, options }) => {
165
185
  hint: "Take ids from folio changes.",
166
186
  details: { missing }
167
187
  }));
168
- for (const id of selected) if (!(action === "accept" ? reviewer.value.acceptChange(Number(id)) : reviewer.value.rejectChange(Number(id)))) return Result.err(cliError({
188
+ for (const id of selected) if (!(action === "accept" ? reviewer.acceptChange(Number(id)) : reviewer.rejectChange(Number(id)))) return Result.err(cliError({
169
189
  code: FOLIO_CLI_ERROR_CODES.operationRejected,
170
190
  message: `Change ${id} could not be ${action}ed; nothing was written.`
171
191
  }));
172
192
  resolved = selected;
173
193
  }
174
- const saved = await saveReviewer(reviewer.value, options.repack);
194
+ const saved = await saveReviewer(reviewer, options.repack);
175
195
  if (saved.isErr()) return Result.err(saved.error);
176
196
  return Result.ok({
177
197
  ...saved.value,
178
198
  result: {
179
199
  action,
180
200
  resolved,
181
- remaining: reviewer.value.getChanges().length
201
+ remaining: reviewer.getChanges().length
182
202
  },
183
203
  receipts: []
184
204
  });
@@ -0,0 +1,27 @@
1
+ import { ToolCallResult, ToolError, ToolInputIssue, ToolOutcome } from "./types.js";
2
+ //#region src/mcp-kit/envelope.d.ts
3
+ /** Error codes the surface itself answers with; a tool adds its own. */
4
+ declare const KIT_ERROR_CODES: {
5
+ readonly validation: "validation_error";
6
+ readonly unknownTool: "unknown_tool";
7
+ readonly notFound: "not_found";
8
+ readonly internal: "internal_error";
9
+ };
10
+ declare const toolError: (error: {
11
+ code: string;
12
+ message: string;
13
+ hint?: string | undefined;
14
+ retryable?: boolean | undefined;
15
+ issues?: readonly ToolInputIssue[] | undefined;
16
+ details?: unknown;
17
+ }) => ToolError;
18
+ declare const failure: (error: Parameters<typeof toolError>[0]) => ToolOutcome;
19
+ declare const success: (value: unknown) => ToolOutcome;
20
+ declare const validationError: (message: string, issues: readonly ToolInputIssue[], hint?: string) => ToolOutcome;
21
+ /** Encode an outcome as a `tools/call` result, with the input notes after a success. */
22
+ declare const toCallResult: (outcome: ToolOutcome, notes?: readonly string[]) => ToolCallResult;
23
+ /** Up to three known names close to `name`, closest first. */
24
+ declare const closestNames: (name: string, known: readonly string[]) => string[];
25
+ declare const didYouMean: (suggestions: readonly string[]) => string;
26
+ //#endregion
27
+ export { KIT_ERROR_CODES, closestNames, didYouMean, failure, success, toCallResult, toolError, validationError };
@@ -0,0 +1,80 @@
1
+ //#region src/mcp-kit/envelope.ts
2
+ /** Error codes the surface itself answers with; a tool adds its own. */
3
+ const KIT_ERROR_CODES = {
4
+ validation: "validation_error",
5
+ unknownTool: "unknown_tool",
6
+ notFound: "not_found",
7
+ internal: "internal_error"
8
+ };
9
+ const toolError = (error) => ({
10
+ code: error.code,
11
+ message: error.message,
12
+ ...error.hint !== void 0 && { hint: error.hint },
13
+ retryable: error.retryable ?? false,
14
+ ...error.issues !== void 0 && error.issues.length > 0 && { issues: error.issues },
15
+ ...error.details !== void 0 && { details: error.details }
16
+ });
17
+ const failure = (error) => ({
18
+ ok: false,
19
+ error: toolError(error)
20
+ });
21
+ const success = (value) => ({
22
+ ok: true,
23
+ value
24
+ });
25
+ const validationError = (message, issues, hint) => failure({
26
+ code: KIT_ERROR_CODES.validation,
27
+ message,
28
+ issues,
29
+ hint,
30
+ retryable: true
31
+ });
32
+ /** Encode an outcome as a `tools/call` result, with the input notes after a success. */
33
+ const toCallResult = (outcome, notes = []) => {
34
+ if (!outcome.ok) return {
35
+ content: [{
36
+ type: "text",
37
+ text: JSON.stringify({ error: outcome.error })
38
+ }],
39
+ isError: true
40
+ };
41
+ const content = [{
42
+ type: "text",
43
+ text: JSON.stringify(outcome.value ?? null)
44
+ }];
45
+ const distinct = [...new Set(notes)];
46
+ if (distinct.length > 0) content.push({
47
+ type: "text",
48
+ text: `Input read: ${distinct.join(" ")}`
49
+ });
50
+ return {
51
+ content,
52
+ isError: false
53
+ };
54
+ };
55
+ const editDistance = (a, b) => {
56
+ let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
57
+ for (let i = 1; i <= a.length; i += 1) {
58
+ const current = [i];
59
+ for (let j = 1; j <= b.length; j += 1) {
60
+ const substitution = (previous[j - 1] ?? 0) + (a[i - 1] === b[j - 1] ? 0 : 1);
61
+ current[j] = Math.min((previous[j] ?? 0) + 1, (current[j - 1] ?? 0) + 1, substitution);
62
+ }
63
+ previous = current;
64
+ }
65
+ return previous[b.length] ?? 0;
66
+ };
67
+ /** Up to three known names close to `name`, closest first. */
68
+ const closestNames = (name, known) => {
69
+ const lowered = name.toLowerCase();
70
+ return known.map((candidate) => {
71
+ const lower = candidate.toLowerCase();
72
+ return {
73
+ candidate,
74
+ distance: lower.includes(lowered) || lowered.includes(lower) ? 0 : editDistance(lowered, lower)
75
+ };
76
+ }).filter(({ distance }) => distance <= Math.max(2, Math.floor(name.length / 3))).toSorted((a, b) => a.distance - b.distance || a.candidate.localeCompare(b.candidate)).slice(0, 3).map(({ candidate }) => candidate);
77
+ };
78
+ const didYouMean = (suggestions) => suggestions.length === 0 ? "" : `Did you mean ${suggestions.map((name) => `"${name}"`).join(" or ")}? `;
79
+ //#endregion
80
+ export { KIT_ERROR_CODES, closestNames, didYouMean, failure, success, toCallResult, toolError, validationError };
@@ -0,0 +1,6 @@
1
+ import { JsonSchema, ListedTool, ToolAccess, ToolCallResult, ToolDefinition, ToolError, ToolInputIssue, ToolOutcome } from "./types.js";
2
+ import { KIT_ERROR_CODES, closestNames, didYouMean, failure, success, toCallResult, toolError, validationError } from "./envelope.js";
3
+ import { ReadInput, readToolInput } from "./input.js";
4
+ import { CompactSchemaOptions, advertisedBytes, compactSchema, hoistRepeatedSchemas } from "./schema.js";
5
+ import { CAPABILITY_TOOL_NAMES, ToolSurface, ToolSurfaceOptions, createToolSurface } from "./surface.js";
6
+ export { CAPABILITY_TOOL_NAMES, type CompactSchemaOptions, type JsonSchema, KIT_ERROR_CODES, type ListedTool, type ReadInput, type ToolAccess, type ToolCallResult, type ToolDefinition, type ToolError, type ToolInputIssue, type ToolOutcome, type ToolSurface, type ToolSurfaceOptions, advertisedBytes, closestNames, compactSchema, createToolSurface, didYouMean, failure, hoistRepeatedSchemas, readToolInput, success, toCallResult, toolError, validationError };
@@ -0,0 +1,5 @@
1
+ import { KIT_ERROR_CODES, closestNames, didYouMean, failure, success, toCallResult, toolError, validationError } from "./envelope.js";
2
+ import { readToolInput } from "./input.js";
3
+ import { advertisedBytes, compactSchema, hoistRepeatedSchemas } from "./schema.js";
4
+ import { CAPABILITY_TOOL_NAMES, createToolSurface } from "./surface.js";
5
+ export { CAPABILITY_TOOL_NAMES, KIT_ERROR_CODES, advertisedBytes, closestNames, compactSchema, createToolSurface, didYouMean, failure, hoistRepeatedSchemas, readToolInput, success, toCallResult, toolError, validationError };
@@ -0,0 +1,23 @@
1
+ import { JsonSchema, ToolAccess, ToolInputIssue } from "./types.js";
2
+ //#region src/mcp-kit/input.d.ts
3
+ type ReadInput = {
4
+ ok: true;
5
+ value: Record<string, unknown>;
6
+ notes: readonly string[];
7
+ } | {
8
+ ok: false;
9
+ message: string;
10
+ issues: readonly ToolInputIssue[];
11
+ hint: string;
12
+ };
13
+ type ReadInputOptions = {
14
+ schema: JsonSchema;
15
+ value: unknown;
16
+ access: ToolAccess;
17
+ /** Properties passed through exactly as sent. */
18
+ exactProperties?: readonly string[] | undefined;
19
+ };
20
+ /** Check and leniently read one call's arguments against the tool's schema. */
21
+ declare const readToolInput: ({ schema, value, access, exactProperties }: ReadInputOptions) => ReadInput;
22
+ //#endregion
23
+ export { ReadInput, readToolInput };
@@ -0,0 +1,76 @@
1
+ import { normalizeAgentInput } from "@stll/agent-input";
2
+ //#region src/mcp-kit/input.ts
3
+ /**
4
+ * Reading a model's arguments before a tool sees them: `null` in an optional
5
+ * property is absent, spellings that carry one meaning (`"true"`, `"20"`, an
6
+ * enum value in another case) are read through `@stll/agent-input` with a
7
+ * note, anything that carries two is asked about, and an argument the schema
8
+ * does not declare, or a required one that is missing, is refused rather than
9
+ * dropped.
10
+ */
11
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
12
+ const propertiesOf = (schema) => isRecord(schema["properties"]) ? schema["properties"] : {};
13
+ const requiredOf = (schema) => Array.isArray(schema["required"]) ? schema["required"].filter((key) => typeof key === "string") : [];
14
+ /**
15
+ * A placeholder in an optional property of a read is "not filtering" and is
16
+ * dropped with a note; on a write it may be an id that changes what the call
17
+ * does, so it is asked about.
18
+ */
19
+ const PLACEHOLDER_POLICY = {
20
+ read: "absent",
21
+ write: "ask"
22
+ };
23
+ const refusal = (message, issues, hint) => ({
24
+ ok: false,
25
+ message,
26
+ issues,
27
+ hint
28
+ });
29
+ /** Check and leniently read one call's arguments against the tool's schema. */
30
+ const readToolInput = ({ schema, value, access, exactProperties = [] }) => {
31
+ if (value !== void 0 && !isRecord(value)) return refusal("Arguments must be a JSON object.", [], "Send the arguments as an object.");
32
+ const properties = propertiesOf(schema);
33
+ const required = requiredOf(schema);
34
+ const input = {};
35
+ const unknown = [];
36
+ for (const [key, entry] of Object.entries(value ?? {})) {
37
+ if (entry === void 0) continue;
38
+ if (!Object.hasOwn(properties, key)) unknown.push({
39
+ path: key,
40
+ message: `Unknown parameter: ${key}`
41
+ });
42
+ else if (entry !== null || required.includes(key)) Object.defineProperty(input, key, {
43
+ value: entry,
44
+ enumerable: true,
45
+ writable: true,
46
+ configurable: true
47
+ });
48
+ }
49
+ if (unknown.length > 0) return refusal("Arguments need clarification.", unknown, `Accepted parameters: ${Object.keys(properties).join(", ")}.`);
50
+ const exact = new Set(exactProperties);
51
+ const normalized = normalizeAgentInput({
52
+ placeholders: PLACEHOLDER_POLICY[access],
53
+ schema: {
54
+ ...schema,
55
+ properties: Object.fromEntries(Object.entries(properties).map(([key, property]) => [key, exact.has(key) ? {} : property]))
56
+ },
57
+ value: input
58
+ });
59
+ if (!normalized.ok) return refusal("Arguments need clarification.", normalized.issues.map(({ path, received, expected }) => ({
60
+ path,
61
+ message: `${received} is not ${expected}.`
62
+ })), normalized.issues.map(({ path, hint }) => path.length === 0 ? hint : `${path}: ${hint}`).join(" "));
63
+ const read = isRecord(normalized.value) ? normalized.value : {};
64
+ const missing = required.filter((key) => read[key] === void 0).map((key) => ({
65
+ path: key,
66
+ message: `Missing parameter: ${key}`
67
+ }));
68
+ if (missing.length > 0) return refusal("Arguments are incomplete.", missing, `Required: ${required.join(", ")}.`);
69
+ return {
70
+ ok: true,
71
+ value: read,
72
+ notes: normalized.notes
73
+ };
74
+ };
75
+ //#endregion
76
+ export { readToolInput };
@@ -0,0 +1,20 @@
1
+ import { JsonSchema, ListedTool } from "./types.js";
2
+ //#region src/mcp-kit/schema.d.ts
3
+ type CompactSchemaOptions = {
4
+ /** Keep descriptions this many property levels deep; 0 (the default) keeps none. */
5
+ describedDepth?: number;
6
+ };
7
+ /** The schema without annotation keywords, descriptions kept only as deep as asked. */
8
+ declare const compactSchema: (schema: JsonSchema, { describedDepth }?: CompactSchemaOptions) => JsonSchema;
9
+ /**
10
+ * Hoist every object subschema that occurs more than once and serializes to
11
+ * at least `minBytes` into `$defs`, referenced by `$ref`: a schema that
12
+ * repeats one shape for several properties states it once.
13
+ */
14
+ declare const hoistRepeatedSchemas: (schema: JsonSchema, { minBytes }?: {
15
+ minBytes?: number;
16
+ }) => JsonSchema;
17
+ /** The UTF-8 size of what `tools/list` sends for these tools. */
18
+ declare const advertisedBytes: (tools: readonly ListedTool[]) => number;
19
+ //#endregion
20
+ export { CompactSchemaOptions, advertisedBytes, compactSchema, hoistRepeatedSchemas };
@@ -0,0 +1,97 @@
1
+ //#region src/mcp-kit/schema.ts
2
+ const ANNOTATION_KEYWORDS = /* @__PURE__ */ new Set([
3
+ "$comment",
4
+ "$schema",
5
+ "default",
6
+ "description",
7
+ "examples",
8
+ "title"
9
+ ]);
10
+ /** Keywords whose value is a map of property name to schema. */
11
+ const SCHEMA_MAP_KEYWORDS = /* @__PURE__ */ new Set([
12
+ "$defs",
13
+ "definitions",
14
+ "patternProperties",
15
+ "properties"
16
+ ]);
17
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
18
+ /**
19
+ * `describedDepth` keeps `description` on schemas that many property maps
20
+ * down (1: the tool's own arguments) and strips it below, where a large
21
+ * nested schema repeats itself.
22
+ */
23
+ const compactNode = (node, depth, describedDepth) => {
24
+ if (Array.isArray(node)) return node.map((item) => compactNode(item, depth, describedDepth));
25
+ if (!isRecord(node)) return node;
26
+ const compacted = {};
27
+ for (const [key, value] of Object.entries(node)) {
28
+ const keptDescription = key === "description" && depth > 0 && depth <= describedDepth;
29
+ if (ANNOTATION_KEYWORDS.has(key) && !keptDescription) continue;
30
+ if (key === "maximum" && value === Number.MAX_SAFE_INTEGER) continue;
31
+ compacted[key] = SCHEMA_MAP_KEYWORDS.has(key) && isRecord(value) ? Object.fromEntries(Object.entries(value).map(([name, schema]) => [name, compactNode(schema, depth + 1, describedDepth)])) : compactNode(value, depth, describedDepth);
32
+ }
33
+ return compacted;
34
+ };
35
+ /** The schema without annotation keywords, descriptions kept only as deep as asked. */
36
+ const compactSchema = (schema, { describedDepth = 0 } = {}) => {
37
+ const compacted = compactNode(schema, 0, describedDepth);
38
+ return isRecord(compacted) ? compacted : {};
39
+ };
40
+ /** Count every object subschema below the root by its serialization, naming each by where it first appears. */
41
+ const countSubschemas = (node, name, seen, isRoot) => {
42
+ if (Array.isArray(node)) {
43
+ for (const item of node) countSubschemas(item, name, seen, false);
44
+ return;
45
+ }
46
+ if (!isRecord(node)) return;
47
+ if (!isRoot) {
48
+ const key = JSON.stringify(node);
49
+ const occurrence = seen.get(key);
50
+ if (occurrence === void 0) seen.set(key, {
51
+ count: 1,
52
+ name,
53
+ bytes: key.length
54
+ });
55
+ else occurrence.count += 1;
56
+ }
57
+ for (const [key, value] of Object.entries(node)) if (SCHEMA_MAP_KEYWORDS.has(key) && isRecord(value)) for (const [child, schema] of Object.entries(value)) countSubschemas(schema, child, seen, false);
58
+ else countSubschemas(value, name, seen, false);
59
+ };
60
+ /**
61
+ * Hoist every object subschema that occurs more than once and serializes to
62
+ * at least `minBytes` into `$defs`, referenced by `$ref`: a schema that
63
+ * repeats one shape for several properties states it once.
64
+ */
65
+ const hoistRepeatedSchemas = (schema, { minBytes = 200 } = {}) => {
66
+ const seen = /* @__PURE__ */ new Map();
67
+ countSubschemas(schema, "schema", seen, true);
68
+ const refs = /* @__PURE__ */ new Map();
69
+ const defs = {};
70
+ const repeated = [...seen.entries()].filter(([, { count, bytes }]) => count > 1 && bytes >= minBytes).toSorted(([, a], [, b]) => b.bytes - a.bytes);
71
+ for (const [key, { name }] of repeated) {
72
+ let defName = name;
73
+ for (let suffix = 2; defName in defs; suffix += 1) defName = `${name}${suffix}`;
74
+ refs.set(key, defName);
75
+ defs[defName] = null;
76
+ }
77
+ if (refs.size === 0) return schema;
78
+ const replace = (node, isRoot) => {
79
+ if (Array.isArray(node)) return node.map((item) => replace(item, false));
80
+ if (!isRecord(node)) return node;
81
+ const ref = isRoot ? void 0 : refs.get(JSON.stringify(node));
82
+ if (ref !== void 0) return { $ref: `#/$defs/${ref}` };
83
+ const replaced = {};
84
+ for (const [key, value] of Object.entries(node)) replaced[key] = SCHEMA_MAP_KEYWORDS.has(key) && isRecord(value) ? Object.fromEntries(Object.entries(value).map(([child, sub]) => [child, replace(sub, false)])) : replace(value, false);
85
+ return replaced;
86
+ };
87
+ for (const [key, defName] of refs) defs[defName] = replace(JSON.parse(key), true);
88
+ const root = replace(schema, true);
89
+ return isRecord(root) ? {
90
+ ...root,
91
+ $defs: defs
92
+ } : schema;
93
+ };
94
+ /** The UTF-8 size of what `tools/list` sends for these tools. */
95
+ const advertisedBytes = (tools) => new TextEncoder().encode(JSON.stringify(tools)).length;
96
+ //#endregion
97
+ export { advertisedBytes, compactSchema, hoistRepeatedSchemas };
@@ -0,0 +1,19 @@
1
+ import { ListedTool, ToolCallResult, ToolDefinition } from "./types.js";
2
+ //#region src/mcp-kit/surface.d.ts
3
+ declare const CAPABILITY_TOOL_NAMES: {
4
+ readonly list: "list_capabilities";
5
+ readonly describe: "describe_capability";
6
+ readonly invoke: "invoke_capability";
7
+ };
8
+ type ToolSurfaceOptions<Context> = {
9
+ readonly tools: readonly ToolDefinition<Context>[];
10
+ };
11
+ type ToolSurface<Context> = {
12
+ /** What `tools/list` returns: the direct tools, then the capability tools. */
13
+ readonly listTools: () => ListedTool[];
14
+ readonly callTool: (name: string, args: unknown, context: Context) => Promise<ToolCallResult>;
15
+ };
16
+ /** Build a surface over `tools`. Names must be unique and not a capability tool's. */
17
+ declare const createToolSurface: <Context>({ tools }: ToolSurfaceOptions<Context>) => ToolSurface<Context>;
18
+ //#endregion
19
+ export { CAPABILITY_TOOL_NAMES, ToolSurface, ToolSurfaceOptions, createToolSurface };