@lunchbox-labs/opencode-tool-inspector 0.1.0 → 0.2.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/README.md +14 -2
- package/dist/agent-text.d.ts +4 -0
- package/dist/agent-text.js +85 -0
- package/dist/agents.d.ts +79 -0
- package/dist/agents.js +244 -0
- package/dist/detail-viewer.d.ts +6 -0
- package/dist/detail-viewer.js +123 -0
- package/dist/schema-text.d.ts +10 -0
- package/dist/schema-text.js +429 -0
- package/dist/tool-detail.d.ts +17 -0
- package/dist/tool-detail.js +41 -0
- package/dist/tools.d.ts +34 -0
- package/dist/tools.js +119 -0
- package/dist/tui.js +409 -1
- package/package.json +1 -1
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
const indentWidth = 2;
|
|
2
|
+
const maximumDepth = 8;
|
|
3
|
+
const maximumLines = 400;
|
|
4
|
+
function isObject(value) {
|
|
5
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
function literal(value) {
|
|
8
|
+
const json = JSON.stringify(value);
|
|
9
|
+
return json ?? String(value);
|
|
10
|
+
}
|
|
11
|
+
function plain(text) {
|
|
12
|
+
return { text, tone: "plain" };
|
|
13
|
+
}
|
|
14
|
+
function indentText(depth) {
|
|
15
|
+
return " ".repeat(depth * indentWidth);
|
|
16
|
+
}
|
|
17
|
+
function descriptionLines(description, depth) {
|
|
18
|
+
if (typeof description !== "string" || description.trim() === "") {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
return description
|
|
22
|
+
.trim()
|
|
23
|
+
.split(/\r?\n/)
|
|
24
|
+
.map((line) => [plain(`${indentText(depth)}${line}`)]);
|
|
25
|
+
}
|
|
26
|
+
function withoutReference(node) {
|
|
27
|
+
const { $ref: _reference, ...rest } = node;
|
|
28
|
+
return rest;
|
|
29
|
+
}
|
|
30
|
+
function decodedPointerKey(rawKey) {
|
|
31
|
+
try {
|
|
32
|
+
return decodeURIComponent(rawKey);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return rawKey;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Resolves local `$ref`s, as zod and MCP servers emit for shared definitions. */
|
|
39
|
+
function resolveReference(node, root, visiting) {
|
|
40
|
+
const reference = node.$ref;
|
|
41
|
+
if (typeof reference !== "string" || !reference.startsWith("#")) {
|
|
42
|
+
return { node, recursive: false };
|
|
43
|
+
}
|
|
44
|
+
if (visiting.has(reference)) {
|
|
45
|
+
return { node, reference, recursive: true };
|
|
46
|
+
}
|
|
47
|
+
let target = root;
|
|
48
|
+
for (const rawKey of reference.slice(1).split("/").filter(Boolean)) {
|
|
49
|
+
const key = decodedPointerKey(rawKey)
|
|
50
|
+
.replaceAll("~1", "/")
|
|
51
|
+
.replaceAll("~0", "~");
|
|
52
|
+
target = isObject(target) ? target[key] : undefined;
|
|
53
|
+
}
|
|
54
|
+
return isObject(target)
|
|
55
|
+
? {
|
|
56
|
+
node: { ...target, ...withoutReference(node) },
|
|
57
|
+
reference,
|
|
58
|
+
recursive: false,
|
|
59
|
+
}
|
|
60
|
+
: { node, reference, recursive: false };
|
|
61
|
+
}
|
|
62
|
+
function referenceName(reference) {
|
|
63
|
+
const name = reference.split("/").at(-1) ?? "";
|
|
64
|
+
return name === "" || name === "#" ? "the whole input" : name;
|
|
65
|
+
}
|
|
66
|
+
function nonEmptyList(value) {
|
|
67
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
68
|
+
const items = value;
|
|
69
|
+
return items;
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
function variants(node) {
|
|
74
|
+
return nonEmptyList(node.anyOf) ?? nonEmptyList(node.oneOf);
|
|
75
|
+
}
|
|
76
|
+
function hasCombinators(node) {
|
|
77
|
+
return variants(node) !== undefined || nonEmptyList(node.allOf) !== undefined;
|
|
78
|
+
}
|
|
79
|
+
/** Folds `allOf` members' properties and required names into one object schema. */
|
|
80
|
+
function mergedAllOf(node, root, visiting) {
|
|
81
|
+
const members = nonEmptyList(node.allOf);
|
|
82
|
+
if (members === undefined) {
|
|
83
|
+
return node;
|
|
84
|
+
}
|
|
85
|
+
const properties = {};
|
|
86
|
+
const required = new Set();
|
|
87
|
+
for (const member of [...members, node]) {
|
|
88
|
+
if (!isObject(member)) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const resolved = resolveReference(member, root, visiting);
|
|
92
|
+
const memberVisiting = resolved.reference === undefined
|
|
93
|
+
? visiting
|
|
94
|
+
: new Set([...visiting, resolved.reference]);
|
|
95
|
+
const memberNode = member === node
|
|
96
|
+
? member
|
|
97
|
+
: resolved.recursive
|
|
98
|
+
? {}
|
|
99
|
+
: mergedAllOf(resolved.node, root, memberVisiting);
|
|
100
|
+
if (isObject(memberNode.properties)) {
|
|
101
|
+
Object.assign(properties, memberNode.properties);
|
|
102
|
+
}
|
|
103
|
+
if (Array.isArray(memberNode.required)) {
|
|
104
|
+
const memberRequired = memberNode.required;
|
|
105
|
+
for (const name of memberRequired) {
|
|
106
|
+
required.add(name);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const { allOf: _allOf, ...rest } = node;
|
|
111
|
+
return Object.keys(properties).length === 0
|
|
112
|
+
? rest
|
|
113
|
+
: { ...rest, properties, required: [...required] };
|
|
114
|
+
}
|
|
115
|
+
function pluralType(type) {
|
|
116
|
+
if (type === "any") {
|
|
117
|
+
return "values";
|
|
118
|
+
}
|
|
119
|
+
if (type.startsWith('"') || type.includes(" ")) {
|
|
120
|
+
return `items of ${type}`;
|
|
121
|
+
}
|
|
122
|
+
return type.endsWith("s") ? type : `${type}s`;
|
|
123
|
+
}
|
|
124
|
+
function typeName(schema, root, visiting, depth) {
|
|
125
|
+
if (schema === true ||
|
|
126
|
+
(isObject(schema) && Object.keys(schema).length === 0)) {
|
|
127
|
+
return "any";
|
|
128
|
+
}
|
|
129
|
+
if (schema === false) {
|
|
130
|
+
return "never";
|
|
131
|
+
}
|
|
132
|
+
if (!isObject(schema)) {
|
|
133
|
+
return "unknown";
|
|
134
|
+
}
|
|
135
|
+
if (depth > maximumDepth) {
|
|
136
|
+
return "…";
|
|
137
|
+
}
|
|
138
|
+
const resolved = resolveReference(schema, root, visiting);
|
|
139
|
+
if (resolved.recursive && resolved.reference !== undefined) {
|
|
140
|
+
return referenceName(resolved.reference);
|
|
141
|
+
}
|
|
142
|
+
const nextVisiting = resolved.reference === undefined
|
|
143
|
+
? visiting
|
|
144
|
+
: new Set([...visiting, resolved.reference]);
|
|
145
|
+
const node = mergedAllOf(resolved.node, root, nextVisiting);
|
|
146
|
+
const nestedName = (nested) => typeName(nested, root, nextVisiting, depth + 1);
|
|
147
|
+
const arrayName = () => {
|
|
148
|
+
if (node.items === undefined) {
|
|
149
|
+
return "array";
|
|
150
|
+
}
|
|
151
|
+
if (Array.isArray(node.items)) {
|
|
152
|
+
const items = node.items;
|
|
153
|
+
return `tuple of ${items.map(nestedName).join(", ")}`;
|
|
154
|
+
}
|
|
155
|
+
return `array of ${pluralType(nestedName(node.items))}`;
|
|
156
|
+
};
|
|
157
|
+
if (Object.hasOwn(node, "const")) {
|
|
158
|
+
return literal(node.const);
|
|
159
|
+
}
|
|
160
|
+
if (Array.isArray(node.enum) && node.enum.length > 0) {
|
|
161
|
+
return node.enum.map(literal).join(" | ");
|
|
162
|
+
}
|
|
163
|
+
const alternatives = variants(node);
|
|
164
|
+
if (alternatives !== undefined) {
|
|
165
|
+
return [...new Set(alternatives.map(nestedName))].join(" | ");
|
|
166
|
+
}
|
|
167
|
+
const type = node.type;
|
|
168
|
+
if (Array.isArray(type)) {
|
|
169
|
+
const names = type
|
|
170
|
+
.filter((item) => typeof item === "string")
|
|
171
|
+
.map((item) => (item === "array" ? arrayName() : item));
|
|
172
|
+
return names.length === 0 ? "any" : names.join(" | ");
|
|
173
|
+
}
|
|
174
|
+
if (type === "array") {
|
|
175
|
+
return arrayName();
|
|
176
|
+
}
|
|
177
|
+
if (type === "object" || isObject(node.properties)) {
|
|
178
|
+
if (!isObject(node.properties) && isObject(node.additionalProperties)) {
|
|
179
|
+
return `object with ${nestedName(node.additionalProperties)} values`;
|
|
180
|
+
}
|
|
181
|
+
return "object";
|
|
182
|
+
}
|
|
183
|
+
return typeof type === "string" ? type : "any";
|
|
184
|
+
}
|
|
185
|
+
const unboundedLimits = new Set([
|
|
186
|
+
Number.MAX_SAFE_INTEGER,
|
|
187
|
+
Number.MIN_SAFE_INTEGER,
|
|
188
|
+
Number.MAX_VALUE,
|
|
189
|
+
-Number.MAX_VALUE,
|
|
190
|
+
]);
|
|
191
|
+
function constraintNotes(node) {
|
|
192
|
+
const notes = [];
|
|
193
|
+
const bound = (key, symbol) => {
|
|
194
|
+
const value = node[key];
|
|
195
|
+
if (typeof value === "number" && !unboundedLimits.has(value)) {
|
|
196
|
+
notes.push(`${symbol} ${value}`);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
bound("minimum", "≥");
|
|
200
|
+
bound("exclusiveMinimum", ">");
|
|
201
|
+
bound("maximum", "≤");
|
|
202
|
+
bound("exclusiveMaximum", "<");
|
|
203
|
+
const count = (key, label) => {
|
|
204
|
+
const value = node[key];
|
|
205
|
+
if (typeof value === "number") {
|
|
206
|
+
notes.push(`${label} ${value}`);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
count("minLength", "min length");
|
|
210
|
+
count("maxLength", "max length");
|
|
211
|
+
count("minItems", "min items");
|
|
212
|
+
count("maxItems", "max items");
|
|
213
|
+
if (typeof node.format === "string") {
|
|
214
|
+
notes.push(`format ${node.format}`);
|
|
215
|
+
}
|
|
216
|
+
if (typeof node.pattern === "string") {
|
|
217
|
+
notes.push(`pattern "${node.pattern.replaceAll("\n", "\\n")}"`);
|
|
218
|
+
}
|
|
219
|
+
if (Object.hasOwn(node, "default")) {
|
|
220
|
+
notes.push(`default ${literal(node.default)}`);
|
|
221
|
+
}
|
|
222
|
+
if (node.deprecated === true) {
|
|
223
|
+
notes.push("deprecated");
|
|
224
|
+
}
|
|
225
|
+
return notes;
|
|
226
|
+
}
|
|
227
|
+
/** The object schema whose properties appear nested under a property, if any. */
|
|
228
|
+
function nestedObject(node, root, visiting) {
|
|
229
|
+
const resolved = resolveReference(node, root, visiting);
|
|
230
|
+
if (resolved.recursive) {
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
const nextVisiting = resolved.reference === undefined
|
|
234
|
+
? visiting
|
|
235
|
+
: new Set([...visiting, resolved.reference]);
|
|
236
|
+
const merged = mergedAllOf(resolved.node, root, nextVisiting);
|
|
237
|
+
// An empty `properties` beside a union, as some schema generators emit, defers to the union.
|
|
238
|
+
if (isObject(merged.properties) &&
|
|
239
|
+
(Object.keys(merged.properties).length > 0 ||
|
|
240
|
+
variants(merged) === undefined)) {
|
|
241
|
+
return { node: merged, visiting: nextVisiting };
|
|
242
|
+
}
|
|
243
|
+
const items = merged.items;
|
|
244
|
+
if (isObject(items)) {
|
|
245
|
+
return nestedObject(items, root, nextVisiting);
|
|
246
|
+
}
|
|
247
|
+
const alternatives = variants(merged);
|
|
248
|
+
if (alternatives !== undefined) {
|
|
249
|
+
const objects = alternatives
|
|
250
|
+
.filter(isObject)
|
|
251
|
+
.map((variant) => nestedObject(variant, root, nextVisiting))
|
|
252
|
+
.filter((variant) => variant !== undefined);
|
|
253
|
+
return objects.length === 1 ? objects[0] : undefined;
|
|
254
|
+
}
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
function muted(text) {
|
|
258
|
+
return { text, tone: "muted" };
|
|
259
|
+
}
|
|
260
|
+
function listedObjectVariants(node, root, visiting) {
|
|
261
|
+
return (variants(node) ?? []).filter((variant) => isObject(variant) && nestedObject(variant, root, visiting) !== undefined).length;
|
|
262
|
+
}
|
|
263
|
+
/** Whether listing a union option says more than "object". */
|
|
264
|
+
function informativeVariant(variant, root, visiting) {
|
|
265
|
+
const type = typeName(variant, root, visiting, 0);
|
|
266
|
+
return ((type !== "object" && type !== "any") ||
|
|
267
|
+
(isObject(variant) && nestedObject(variant, root, visiting) !== undefined));
|
|
268
|
+
}
|
|
269
|
+
/** The union of several object options under a property, following array items, if any. */
|
|
270
|
+
function objectUnion(node, root, visiting) {
|
|
271
|
+
const resolved = resolveReference(node, root, visiting);
|
|
272
|
+
if (resolved.recursive) {
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
const nextVisiting = resolved.reference === undefined
|
|
276
|
+
? visiting
|
|
277
|
+
: new Set([...visiting, resolved.reference]);
|
|
278
|
+
const merged = mergedAllOf(resolved.node, root, nextVisiting);
|
|
279
|
+
if (listedObjectVariants(merged, root, nextVisiting) > 1) {
|
|
280
|
+
return { node: merged, visiting: nextVisiting };
|
|
281
|
+
}
|
|
282
|
+
// A nullable union wraps the union beside a null option.
|
|
283
|
+
const present = (variants(merged) ?? []).filter((variant) => !(isObject(variant) && variant.type === "null"));
|
|
284
|
+
if (present.length === 1 && isObject(present[0])) {
|
|
285
|
+
return objectUnion(present[0], root, nextVisiting);
|
|
286
|
+
}
|
|
287
|
+
return isObject(merged.items)
|
|
288
|
+
? objectUnion(merged.items, root, nextVisiting)
|
|
289
|
+
: undefined;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Lists an object schema's properties, then each option of a union beside them, indented by
|
|
293
|
+
* depth.
|
|
294
|
+
*/
|
|
295
|
+
function propertyLines(objectSchema, root, visiting, depth) {
|
|
296
|
+
if (depth > maximumDepth) {
|
|
297
|
+
return [[plain(`${indentText(depth)}…`)]];
|
|
298
|
+
}
|
|
299
|
+
/** Lists each option of a union, or nothing when no option says more than "object". */
|
|
300
|
+
const optionLines = (schema) => {
|
|
301
|
+
const alternatives = variants(schema);
|
|
302
|
+
if (alternatives?.some((variant) => informativeVariant(variant, root, visiting)) !== true) {
|
|
303
|
+
return [];
|
|
304
|
+
}
|
|
305
|
+
return alternatives.flatMap((variant, index) => {
|
|
306
|
+
const heading = [
|
|
307
|
+
plain(indentText(depth)),
|
|
308
|
+
{ text: `Option ${index + 1}`, tone: "name" },
|
|
309
|
+
];
|
|
310
|
+
const nested = isObject(variant)
|
|
311
|
+
? nestedObject(variant, root, visiting)
|
|
312
|
+
: undefined;
|
|
313
|
+
const lines = nested === undefined
|
|
314
|
+
? []
|
|
315
|
+
: propertyLines(nested.node, root, nested.visiting, depth + 1);
|
|
316
|
+
return lines.length > 0
|
|
317
|
+
? [heading, ...lines]
|
|
318
|
+
: [
|
|
319
|
+
[
|
|
320
|
+
...heading,
|
|
321
|
+
muted(` ${typeName(variant, root, visiting, depth)}`),
|
|
322
|
+
],
|
|
323
|
+
];
|
|
324
|
+
});
|
|
325
|
+
};
|
|
326
|
+
const properties = isObject(objectSchema.properties)
|
|
327
|
+
? Object.entries(objectSchema.properties)
|
|
328
|
+
: [];
|
|
329
|
+
const options = optionLines(objectSchema);
|
|
330
|
+
if (properties.length === 0) {
|
|
331
|
+
return options;
|
|
332
|
+
}
|
|
333
|
+
const required = new Set(Array.isArray(objectSchema.required)
|
|
334
|
+
? objectSchema.required.filter((name) => typeof name === "string")
|
|
335
|
+
: []);
|
|
336
|
+
const fields = properties.flatMap(([name, property]) => {
|
|
337
|
+
const resolved = isObject(property)
|
|
338
|
+
? mergedAllOf(resolveReference(property, root, visiting).node, root, visiting)
|
|
339
|
+
: {};
|
|
340
|
+
const details = [
|
|
341
|
+
typeName(property, root, visiting, depth),
|
|
342
|
+
...(required.has(name) ? ["required"] : []),
|
|
343
|
+
...constraintNotes(resolved),
|
|
344
|
+
];
|
|
345
|
+
const heading = [
|
|
346
|
+
plain(indentText(depth)),
|
|
347
|
+
{ text: name, tone: "name" },
|
|
348
|
+
{ text: ` ${details.join(", ")}`, tone: "muted" },
|
|
349
|
+
];
|
|
350
|
+
const nested = isObject(property)
|
|
351
|
+
? nestedObject(property, root, visiting)
|
|
352
|
+
: undefined;
|
|
353
|
+
const union = nested === undefined && isObject(property)
|
|
354
|
+
? objectUnion(property, root, visiting)
|
|
355
|
+
: undefined;
|
|
356
|
+
const childLines = nested !== undefined
|
|
357
|
+
? propertyLines(nested.node, root, nested.visiting, depth + 1)
|
|
358
|
+
: union !== undefined
|
|
359
|
+
? propertyLines(union.node, root, union.visiting, depth + 1)
|
|
360
|
+
: [];
|
|
361
|
+
return [
|
|
362
|
+
heading,
|
|
363
|
+
...descriptionLines(resolved.description, depth + 1),
|
|
364
|
+
...childLines,
|
|
365
|
+
];
|
|
366
|
+
});
|
|
367
|
+
return options.length === 0
|
|
368
|
+
? fields
|
|
369
|
+
: [...fields, [plain(indentText(depth)), muted("And one of:")], ...options];
|
|
370
|
+
}
|
|
371
|
+
function rawSchemaLines(schema) {
|
|
372
|
+
return [
|
|
373
|
+
[muted("OpenCode reported this input schema:")],
|
|
374
|
+
...JSON.stringify(schema, undefined, indentWidth)
|
|
375
|
+
.split("\n")
|
|
376
|
+
.map((line) => [muted(line)]),
|
|
377
|
+
];
|
|
378
|
+
}
|
|
379
|
+
function capped(lines) {
|
|
380
|
+
if (lines.length <= maximumLines) {
|
|
381
|
+
return lines;
|
|
382
|
+
}
|
|
383
|
+
return [
|
|
384
|
+
...lines.slice(0, maximumLines),
|
|
385
|
+
[muted(`… ${lines.length - maximumLines} more lines not shown.`)],
|
|
386
|
+
];
|
|
387
|
+
}
|
|
388
|
+
function rootLines(schema) {
|
|
389
|
+
const nested = nestedObject(schema, schema, new Set());
|
|
390
|
+
if (nested !== undefined) {
|
|
391
|
+
const lines = propertyLines(nested.node, schema, nested.visiting, 0);
|
|
392
|
+
if (lines.length > 0) {
|
|
393
|
+
return lines;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const resolved = resolveReference(schema, schema, new Set()).node;
|
|
397
|
+
const alternatives = variants(mergedAllOf(resolved, schema, new Set()));
|
|
398
|
+
const informative = alternatives?.some((variant) => informativeVariant(variant, schema, new Set()));
|
|
399
|
+
if (alternatives !== undefined && informative === true) {
|
|
400
|
+
return [
|
|
401
|
+
[muted("Takes one of these inputs:")],
|
|
402
|
+
...propertyLines(mergedAllOf(resolved, schema, new Set()), schema, new Set(), 0),
|
|
403
|
+
];
|
|
404
|
+
}
|
|
405
|
+
const type = typeName(schema, schema, new Set(), 0);
|
|
406
|
+
if (type === "object" || type === "any") {
|
|
407
|
+
return hasCombinators(resolved)
|
|
408
|
+
? rawSchemaLines(schema)
|
|
409
|
+
: [[muted("Takes no input.")]];
|
|
410
|
+
}
|
|
411
|
+
return [[plain("Input is "), { text: type, tone: "name" }]];
|
|
412
|
+
}
|
|
413
|
+
/** Renders a tool's input JSON schema as indented, readable lines. */
|
|
414
|
+
export function schemaLines(schema) {
|
|
415
|
+
if (!isObject(schema)) {
|
|
416
|
+
return [[muted("OpenCode did not report an input schema.")]];
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
return capped(rootLines(schema));
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
return capped(rawSchemaLines(schema));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
export function linesText(lines) {
|
|
426
|
+
return lines
|
|
427
|
+
.map((line) => line.map((segment) => segment.text).join(""))
|
|
428
|
+
.join("\n");
|
|
429
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ToolAgentAccess } from "./agents.ts";
|
|
2
|
+
import { type TextLine } from "./schema-text.ts";
|
|
3
|
+
import type { BrowsableTool } from "./tools.ts";
|
|
4
|
+
export interface DetailSection {
|
|
5
|
+
heading: string;
|
|
6
|
+
lines: TextLine[];
|
|
7
|
+
}
|
|
8
|
+
export interface ToolDetail {
|
|
9
|
+
title: string;
|
|
10
|
+
subtitle: string | undefined;
|
|
11
|
+
sections: DetailSection[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The detail view's content. The Agents section is left out when OpenCode's agents couldn't be
|
|
15
|
+
* read.
|
|
16
|
+
*/
|
|
17
|
+
export declare function toolDetail(tool: BrowsableTool, modelLabel: string | undefined, access?: ToolAgentAccess): ToolDetail;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { agentAccessLines } from "./agent-text.js";
|
|
2
|
+
import { schemaLines } from "./schema-text.js";
|
|
3
|
+
function muted(text) {
|
|
4
|
+
return [[{ text, tone: "muted" }]];
|
|
5
|
+
}
|
|
6
|
+
function textLines(text) {
|
|
7
|
+
return text
|
|
8
|
+
.trim()
|
|
9
|
+
.split(/\r?\n/)
|
|
10
|
+
.map((line) => [{ text: line, tone: "plain" }]);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The detail view's content. The Agents section is left out when OpenCode's agents couldn't be
|
|
14
|
+
* read.
|
|
15
|
+
*/
|
|
16
|
+
export function toolDetail(tool, modelLabel, access) {
|
|
17
|
+
const detailsMissing = "OpenCode reported only this tool's name.";
|
|
18
|
+
return {
|
|
19
|
+
title: tool.id,
|
|
20
|
+
subtitle: modelLabel === undefined ? undefined : `As offered to ${modelLabel}`,
|
|
21
|
+
sections: [
|
|
22
|
+
{
|
|
23
|
+
heading: "Input",
|
|
24
|
+
lines: tool.description === undefined && tool.parameters === undefined
|
|
25
|
+
? muted(detailsMissing)
|
|
26
|
+
: schemaLines(tool.parameters),
|
|
27
|
+
},
|
|
28
|
+
...(access === undefined
|
|
29
|
+
? []
|
|
30
|
+
: [{ heading: "Agents", lines: agentAccessLines(access) }]),
|
|
31
|
+
{
|
|
32
|
+
heading: "Description",
|
|
33
|
+
lines: tool.description === undefined
|
|
34
|
+
? muted(detailsMissing)
|
|
35
|
+
: tool.description.trim() === ""
|
|
36
|
+
? muted("No description.")
|
|
37
|
+
: textLines(tool.description),
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
}
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface ModelChoice {
|
|
2
|
+
providerID: string;
|
|
3
|
+
modelID: string;
|
|
4
|
+
}
|
|
5
|
+
/** Where the browsed model came from, since the next prompt may use another. */
|
|
6
|
+
export type ModelSource = "session" | "configured" | "provider default";
|
|
7
|
+
export interface ChosenModel extends ModelChoice {
|
|
8
|
+
source: ModelSource;
|
|
9
|
+
}
|
|
10
|
+
export interface BrowsableTool {
|
|
11
|
+
id: string;
|
|
12
|
+
/** Undefined when OpenCode reported only the tool's id. */
|
|
13
|
+
description: string | undefined;
|
|
14
|
+
parameters: unknown;
|
|
15
|
+
}
|
|
16
|
+
export declare function modelLabel(model: ModelChoice): string;
|
|
17
|
+
export declare function chosenModelLabel(model: ChosenModel): string;
|
|
18
|
+
/** The model of the session's latest user message, which OpenCode resolves tools for on the next prompt. */
|
|
19
|
+
export declare function sessionModel(messages: readonly unknown[]): ModelChoice | undefined;
|
|
20
|
+
/** Parses the `provider/model` form used by the `model` setting. */
|
|
21
|
+
export declare function configuredModel(model: unknown): ModelChoice | undefined;
|
|
22
|
+
/** The first connected provider's default model, which OpenCode falls back to when none is configured. */
|
|
23
|
+
export declare function providerDefaultModel(connectedProviderIds: readonly string[], defaults: unknown): ModelChoice | undefined;
|
|
24
|
+
/** Keeps well-formed entries from OpenCode's tool list, sorted by id. */
|
|
25
|
+
export declare function toolsFromList(response: unknown): BrowsableTool[] | undefined;
|
|
26
|
+
/** Tool ids without details, for when OpenCode can't resolve tools for a model. */
|
|
27
|
+
export declare function toolsFromIds(response: unknown): BrowsableTool[] | undefined;
|
|
28
|
+
/** The first line of a description without its list marker, short enough for one list row. */
|
|
29
|
+
export declare function descriptionSummary(description: string | undefined): string;
|
|
30
|
+
/**
|
|
31
|
+
* The number of top-level input properties, or undefined when the schema has none to count, such
|
|
32
|
+
* as a union beside empty `properties`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function inputCount(parameters: unknown): number | undefined;
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const sourceLabels = {
|
|
2
|
+
session: "this session's model",
|
|
3
|
+
configured: "configured model",
|
|
4
|
+
"provider default": "provider default",
|
|
5
|
+
};
|
|
6
|
+
/** OpenCode's placeholder for unknown tool calls, never offered to agents as a real tool. */
|
|
7
|
+
const internalToolIds = new Set(["invalid"]);
|
|
8
|
+
function isRecord(value) {
|
|
9
|
+
return typeof value === "object" && value !== null;
|
|
10
|
+
}
|
|
11
|
+
export function modelLabel(model) {
|
|
12
|
+
return `${model.providerID}/${model.modelID}`;
|
|
13
|
+
}
|
|
14
|
+
export function chosenModelLabel(model) {
|
|
15
|
+
return `${modelLabel(model)} (${sourceLabels[model.source]})`;
|
|
16
|
+
}
|
|
17
|
+
/** The model of the session's latest user message, which OpenCode resolves tools for on the next prompt. */
|
|
18
|
+
export function sessionModel(messages) {
|
|
19
|
+
for (const message of [...messages].reverse()) {
|
|
20
|
+
if (!isRecord(message) || message.role !== "user") {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const model = message.model;
|
|
24
|
+
if (isRecord(model) &&
|
|
25
|
+
typeof model.providerID === "string" &&
|
|
26
|
+
typeof model.modelID === "string" &&
|
|
27
|
+
model.providerID !== "" &&
|
|
28
|
+
model.modelID !== "") {
|
|
29
|
+
return { providerID: model.providerID, modelID: model.modelID };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
/** Parses the `provider/model` form used by the `model` setting. */
|
|
35
|
+
export function configuredModel(model) {
|
|
36
|
+
if (typeof model !== "string") {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const separator = model.indexOf("/");
|
|
40
|
+
if (separator <= 0 || separator === model.length - 1) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
providerID: model.slice(0, separator),
|
|
45
|
+
modelID: model.slice(separator + 1),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** The first connected provider's default model, which OpenCode falls back to when none is configured. */
|
|
49
|
+
export function providerDefaultModel(connectedProviderIds, defaults) {
|
|
50
|
+
if (!isRecord(defaults)) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
for (const providerID of connectedProviderIds) {
|
|
54
|
+
const modelID = defaults[providerID];
|
|
55
|
+
if (typeof modelID === "string" && modelID !== "") {
|
|
56
|
+
return { providerID, modelID };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
function byId(left, right) {
|
|
62
|
+
return left.id.localeCompare(right.id);
|
|
63
|
+
}
|
|
64
|
+
/** Keeps well-formed entries from OpenCode's tool list, sorted by id. */
|
|
65
|
+
export function toolsFromList(response) {
|
|
66
|
+
if (!Array.isArray(response)) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
const tools = new Map();
|
|
70
|
+
for (const item of response) {
|
|
71
|
+
if (!isRecord(item) ||
|
|
72
|
+
typeof item.id !== "string" ||
|
|
73
|
+
item.id === "" ||
|
|
74
|
+
internalToolIds.has(item.id)) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
tools.set(item.id, {
|
|
78
|
+
id: item.id,
|
|
79
|
+
description: typeof item.description === "string" ? item.description : undefined,
|
|
80
|
+
parameters: item.parameters,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return [...tools.values()].sort(byId);
|
|
84
|
+
}
|
|
85
|
+
/** Tool ids without details, for when OpenCode can't resolve tools for a model. */
|
|
86
|
+
export function toolsFromIds(response) {
|
|
87
|
+
if (!Array.isArray(response)) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
const ids = new Set(response.filter((id) => typeof id === "string" && id !== "" && !internalToolIds.has(id)));
|
|
91
|
+
return [...ids]
|
|
92
|
+
.map((id) => ({ id, description: undefined, parameters: undefined }))
|
|
93
|
+
.sort(byId);
|
|
94
|
+
}
|
|
95
|
+
const summaryLength = 100;
|
|
96
|
+
/** The first line of a description without its list marker, short enough for one list row. */
|
|
97
|
+
export function descriptionSummary(description) {
|
|
98
|
+
const firstLine = description
|
|
99
|
+
?.split(/\r?\n/)
|
|
100
|
+
.map((line) => line.trim().replace(/^[-*•]\s+/, ""))
|
|
101
|
+
.find((line) => line !== "") ?? "";
|
|
102
|
+
return firstLine.length > summaryLength
|
|
103
|
+
? `${firstLine.slice(0, summaryLength - 1).trimEnd()}…`
|
|
104
|
+
: firstLine;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The number of top-level input properties, or undefined when the schema has none to count, such
|
|
108
|
+
* as a union beside empty `properties`.
|
|
109
|
+
*/
|
|
110
|
+
export function inputCount(parameters) {
|
|
111
|
+
if (!isRecord(parameters) || !isRecord(parameters.properties)) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
const count = Object.keys(parameters.properties).length;
|
|
115
|
+
return count === 0 &&
|
|
116
|
+
(parameters.anyOf !== undefined || parameters.oneOf !== undefined)
|
|
117
|
+
? undefined
|
|
118
|
+
: count;
|
|
119
|
+
}
|