@cerebro-labs/agentnava-sdk 0.26.0 → 0.46.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.
- package/README.md +212 -23
- package/account.d.ts +154 -0
- package/account.js +219 -0
- package/account.js.map +1 -0
- package/agent.d.ts +229 -87
- package/agent.js +310 -125
- package/agent.js.map +1 -1
- package/apps.generated.d.ts +154 -0
- package/apps.generated.js +152 -0
- package/apps.generated.js.map +1 -0
- package/attach.d.ts +25 -26
- package/attach.js +53 -66
- package/attach.js.map +1 -1
- package/connection.d.ts +79 -82
- package/connection.js +67 -134
- package/connection.js.map +1 -1
- package/conversation.d.ts +55 -35
- package/conversation.js +70 -50
- package/conversation.js.map +1 -1
- package/database.d.ts +53 -0
- package/database.js +69 -0
- package/database.js.map +1 -0
- package/end-user.d.ts +58 -47
- package/end-user.js +62 -72
- package/end-user.js.map +1 -1
- package/errors.d.ts +2 -1
- package/errors.js +2 -1
- package/errors.js.map +1 -1
- package/http.d.ts +5 -0
- package/http.js +109 -8
- package/http.js.map +1 -1
- package/import-tools.d.ts +54 -33
- package/import-tools.js +526 -124
- package/import-tools.js.map +1 -1
- package/index.d.ts +5 -2
- package/index.js +9 -2
- package/index.js.map +1 -1
- package/load-agent-dir.js +28 -5
- package/load-agent-dir.js.map +1 -1
- package/package.json +5 -1
- package/secret.d.ts +1 -1
- package/secret.js +1 -1
- package/secret.js.map +1 -1
- package/templates-inline.js +1 -1
- package/templates-inline.js.map +1 -1
- package/turn.d.ts +1 -1
- package/turn.js +77 -29
- package/turn.js.map +1 -1
- package/types.d.ts +462 -83
- package/types.js.map +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.js.map +1 -1
- package/workflows.d.ts +10 -0
- package/workflows.js +16 -0
- package/workflows.js.map +1 -1
- package/workspace.d.ts +80 -62
- package/workspace.js +92 -75
- package/workspace.js.map +1 -1
- package/connection-status.d.ts +0 -28
- package/connection-status.js +0 -61
- package/connection-status.js.map +0 -1
package/import-tools.js
CHANGED
|
@@ -199,8 +199,8 @@ class Draft {
|
|
|
199
199
|
* never-sent `order_id_2` in `params`, which is the very "declared and never
|
|
200
200
|
* sent" defect this class is meant to prevent. So a repeat returns the same
|
|
201
201
|
* name and merges the richer definition over the first guess. */
|
|
202
|
-
param(params, wireName, def, tool) {
|
|
203
|
-
const key = `${tool}\u0000${
|
|
202
|
+
param(params, wireName, def, tool, identity = wireName) {
|
|
203
|
+
const key = `${tool}\u0000${identity}`;
|
|
204
204
|
const already = this.#paramNames.get(key);
|
|
205
205
|
if (already !== undefined) {
|
|
206
206
|
params[already] = { ...params[already], ...def };
|
|
@@ -426,146 +426,548 @@ export function fromPostman(collection, options = {}) {
|
|
|
426
426
|
return draft.finish(options);
|
|
427
427
|
}
|
|
428
428
|
const OPENAPI_METHODS = ['get', 'post', 'put', 'delete', 'patch'];
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
429
|
+
/** The object a local `#/...` reference names, one hop. */
|
|
430
|
+
function refTarget(ref, doc) {
|
|
431
|
+
if (!ref.startsWith('#/'))
|
|
432
|
+
throw Error('Only acyclic local OpenAPI references are supported');
|
|
433
|
+
let target = doc;
|
|
434
|
+
for (const key of ref.slice(2).split('/').map(x => x.replace(/~1/g, '/').replace(/~0/g, '~'))) {
|
|
435
|
+
if (!target || typeof target !== 'object' || !Object.hasOwn(target, key))
|
|
436
|
+
throw Error(`Unresolved OpenAPI reference ${ref}`);
|
|
437
|
+
target = target[key];
|
|
438
|
+
}
|
|
439
|
+
if (!target || typeof target !== 'object' || Array.isArray(target))
|
|
440
|
+
throw Error('OpenAPI reference must resolve to an object');
|
|
441
|
+
return target;
|
|
442
|
+
}
|
|
443
|
+
function dereference(value, doc, seen = new Set()) {
|
|
444
|
+
if (!value.$ref)
|
|
445
|
+
return value;
|
|
446
|
+
if (seen.has(value.$ref))
|
|
447
|
+
throw Error('Only acyclic local OpenAPI references are supported');
|
|
448
|
+
seen.add(value.$ref);
|
|
449
|
+
return dereference(refTarget(value.$ref, doc), doc, seen);
|
|
450
|
+
}
|
|
451
|
+
/** Keywords that only narrow which values are valid: a bound, a length, a
|
|
452
|
+
* pattern, a format. A tool parameter has no field for them, so they go into
|
|
453
|
+
* its description for the model, and the API enforces them, as it does for
|
|
454
|
+
* every other caller.
|
|
455
|
+
*
|
|
456
|
+
* THEY USED TO SKIP THE WHOLE OPERATION, even on an optional parameter, on the
|
|
457
|
+
* theory that a tool which cannot enforce a rule must not exist. Real documents
|
|
458
|
+
* put these on almost every field: a generated document with 188 operations
|
|
459
|
+
* drafted 77 tools. The rule was never the tool's to enforce; the API already
|
|
460
|
+
* does, and its 400 tells the model what to fix. */
|
|
461
|
+
const RULE_KEYWORDS = [
|
|
462
|
+
'minimum', 'exclusiveMinimum', 'maximum', 'exclusiveMaximum', 'multipleOf', 'minLength', 'maxLength',
|
|
463
|
+
'pattern', 'format', 'minItems', 'maxItems', 'uniqueItems', 'minProperties', 'maxProperties',
|
|
464
|
+
];
|
|
465
|
+
const PRIMITIVE_TYPES = new Set(['string', 'number', 'integer', 'boolean']);
|
|
466
|
+
const PARAM_TYPES = new Set([...PRIMITIVE_TYPES, 'object', 'array']);
|
|
467
|
+
const TYPE_PHRASE = {
|
|
468
|
+
string: 'a string', number: 'a number', integer: 'an integer', boolean: 'true or false', object: 'an object', array: 'a list',
|
|
469
|
+
};
|
|
470
|
+
const refName = (ref) => ref.slice(ref.lastIndexOf('/') + 1);
|
|
471
|
+
/** Sentences joined into one description. A lone part gains no full stop. */
|
|
472
|
+
function sentences(...parts) {
|
|
473
|
+
const kept = parts.map(p => p?.trim()).filter((p) => !!p);
|
|
474
|
+
if (kept.length <= 1)
|
|
475
|
+
return kept[0];
|
|
476
|
+
return kept.map(p => /[.!?]$/.test(p) ? p : `${p}.`).join(' ');
|
|
477
|
+
}
|
|
478
|
+
/** The type a resolved schema declares, or the one its other keywords imply. */
|
|
479
|
+
function typeOf(s) {
|
|
480
|
+
if (typeof s.type === 'string')
|
|
481
|
+
return s.type;
|
|
482
|
+
if (s.properties !== undefined || s.additionalProperties !== undefined)
|
|
483
|
+
return 'object';
|
|
484
|
+
if (s.items !== undefined)
|
|
485
|
+
return 'array';
|
|
486
|
+
const values = s.enum?.filter(v => v !== null) ?? [];
|
|
487
|
+
if (values.length && values.every(v => typeof v === 'string'))
|
|
488
|
+
return 'string';
|
|
489
|
+
if (values.length && values.every(v => typeof v === 'boolean'))
|
|
433
490
|
return 'boolean';
|
|
434
|
-
|
|
491
|
+
if (values.length && values.every(v => typeof v === 'number'))
|
|
492
|
+
return values.every(Number.isInteger) ? 'integer' : 'number';
|
|
493
|
+
return undefined;
|
|
435
494
|
}
|
|
436
|
-
/**
|
|
437
|
-
|
|
495
|
+
/** The rules a schema carries, in words: "at least 1", "3 to 64 characters". */
|
|
496
|
+
function ruleText(s, type) {
|
|
497
|
+
const n = (key) => typeof s[key] === 'number' && Number.isFinite(s[key]) ? s[key] : undefined;
|
|
498
|
+
const span = (min, max, unit) => {
|
|
499
|
+
const of = (x) => `${x} ${unit}${x === 1 ? '' : 's'}`;
|
|
500
|
+
if (min !== undefined && max !== undefined)
|
|
501
|
+
return min === max ? `exactly ${of(min)}` : `${min} to ${of(max)}`;
|
|
502
|
+
if (min !== undefined)
|
|
503
|
+
return `at least ${of(min)}`;
|
|
504
|
+
return max !== undefined ? `at most ${of(max)}` : undefined;
|
|
505
|
+
};
|
|
506
|
+
const extra = s.additionalProperties;
|
|
507
|
+
const value = extra && typeof extra === 'object'
|
|
508
|
+
? (typeof extra.type === 'string' ? TYPE_PHRASE[extra.type] : typeof extra.$ref === 'string' ? refName(extra.$ref) : undefined)
|
|
509
|
+
: undefined;
|
|
510
|
+
return [
|
|
511
|
+
// 3.0 writes `exclusiveMinimum: true` beside `minimum`; 3.1 makes it the bound.
|
|
512
|
+
n('minimum') !== undefined ? `${s.exclusiveMinimum === true ? 'more than' : 'at least'} ${n('minimum')}` : undefined,
|
|
513
|
+
n('exclusiveMinimum') !== undefined ? `more than ${n('exclusiveMinimum')}` : undefined,
|
|
514
|
+
n('maximum') !== undefined ? `${s.exclusiveMaximum === true ? 'less than' : 'at most'} ${n('maximum')}` : undefined,
|
|
515
|
+
n('exclusiveMaximum') !== undefined ? `less than ${n('exclusiveMaximum')}` : undefined,
|
|
516
|
+
n('multipleOf') !== undefined ? `a multiple of ${n('multipleOf')}` : undefined,
|
|
517
|
+
span(n('minLength'), n('maxLength'), 'character'),
|
|
518
|
+
typeof s.pattern === 'string' ? `pattern \`${s.pattern}\`` : undefined,
|
|
519
|
+
// int32, int64 and double say how a number is stored, not what to send.
|
|
520
|
+
typeof s.format === 'string' && type === 'string' ? `format ${s.format}` : undefined,
|
|
521
|
+
span(n('minItems'), n('maxItems'), 'item'),
|
|
522
|
+
s.uniqueItems === true ? 'no duplicate items' : undefined,
|
|
523
|
+
span(n('minProperties'), n('maxProperties'), 'field'),
|
|
524
|
+
extra && typeof extra === 'object' ? `any keys${value ? `, each value ${value}` : ''}` : undefined,
|
|
525
|
+
].filter((rule) => rule !== undefined);
|
|
526
|
+
}
|
|
527
|
+
/** One flat schema, every reference, composition and alternative folded in.
|
|
528
|
+
* Rules stay on it as keywords, for `ruleText` to put into words; nested
|
|
529
|
+
* properties and items are left for their own turn.
|
|
438
530
|
*
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
|
|
531
|
+
* `nesting` counts references and compositions, separately from how deep the
|
|
532
|
+
* parameter itself goes: the API allows 12 levels of fields, and a document
|
|
533
|
+
* that reaches each level through a $ref and an allOf should not spend three. */
|
|
534
|
+
function resolveOpenApiSchema(raw, ctx, where, nesting = 0) {
|
|
535
|
+
if (nesting > 32)
|
|
536
|
+
throw Error('OpenAPI schema is recursive or nests references more than 32 deep');
|
|
537
|
+
if (typeof raw.$ref === 'string') {
|
|
538
|
+
const { $ref, ...siblings } = raw;
|
|
539
|
+
const target = refTarget($ref, ctx.doc);
|
|
540
|
+
// Keywords beside a $ref apply together with what it names (OpenAPI 3.1),
|
|
541
|
+
// which is also what a 3.0 author writing a description or a rule there means.
|
|
542
|
+
const next = Object.keys(siblings).length ? { ...siblings, allOf: [target, ...(siblings.allOf ?? [])] } : target;
|
|
543
|
+
return resolveOpenApiSchema(next, ctx, where, nesting + 1);
|
|
544
|
+
}
|
|
545
|
+
const { allOf, oneOf, anyOf, ...own } = raw;
|
|
546
|
+
if (allOf && !allOf.length)
|
|
547
|
+
throw Error('An allOf schema needs at least one member');
|
|
548
|
+
const parts = (allOf ?? []).map(part => resolveOpenApiSchema(part, ctx, where, nesting + 1));
|
|
549
|
+
const said = [];
|
|
550
|
+
const join = (alternatives) => {
|
|
551
|
+
const joined = joinAlternatives(alternatives, ctx, where, nesting + 1);
|
|
552
|
+
parts.push(joined.schema);
|
|
553
|
+
if (joined.text)
|
|
554
|
+
said.push(joined.text);
|
|
555
|
+
};
|
|
556
|
+
if (Array.isArray(own.type)) {
|
|
557
|
+
// `type: [a, b]` is anyOf by another name, and "null" in it is nullable.
|
|
558
|
+
const types = own.type.filter(t => t !== 'null');
|
|
559
|
+
if (own.type.includes('null'))
|
|
560
|
+
own.nullable = true;
|
|
561
|
+
if (types.length === 1)
|
|
562
|
+
own.type = types[0];
|
|
563
|
+
else if (types.length) {
|
|
564
|
+
delete own.type;
|
|
565
|
+
join(types.map(type => ({ type })));
|
|
566
|
+
}
|
|
567
|
+
else
|
|
568
|
+
own.type = 'null';
|
|
569
|
+
}
|
|
570
|
+
if (oneOf !== undefined)
|
|
571
|
+
join(oneOf);
|
|
572
|
+
if (anyOf !== undefined)
|
|
573
|
+
join(anyOf);
|
|
574
|
+
const merged = intersect(parts, own);
|
|
575
|
+
const description = sentences(merged.description, ...said);
|
|
576
|
+
return description === undefined ? merged : { ...merged, description };
|
|
577
|
+
}
|
|
578
|
+
/** allOf: every part applies at once. Properties and required fields add up,
|
|
579
|
+
* types and enums must agree, and rules ride along to be described. A genuine
|
|
580
|
+
* contradiction, a value that has to be both a string and a number, is an error
|
|
581
|
+
* rather than a guess. Parts are flat; their properties and items are not yet. */
|
|
582
|
+
function intersect(parts, own) {
|
|
583
|
+
const all = [...parts, own];
|
|
584
|
+
if (all.some(part => part.prefixItems !== undefined))
|
|
585
|
+
throw Error('Tuple arrays (prefixItems) are not supported');
|
|
586
|
+
// Annotations and rules: the outer schema's own word, else the first part's.
|
|
587
|
+
const first = (key) => [own, ...parts].map(part => part[key]).find(value => value !== undefined);
|
|
588
|
+
const types = [...new Set(all.map(part => part.type).filter((t) => typeof t === 'string'))];
|
|
589
|
+
const numeric = types.length === 2 && types.includes('integer') && types.includes('number');
|
|
590
|
+
if (types.length > 1 && !numeric)
|
|
591
|
+
throw Error(`allOf members disagree on the type (${types.join(' and ')})`);
|
|
592
|
+
const enums = all.map(part => part.const !== undefined ? [part.const] : part.enum).filter(e => e !== undefined);
|
|
593
|
+
const values = enums.length ? enums.reduce((kept, e) => kept.filter(v => e.includes(v))) : undefined;
|
|
594
|
+
if (values && !values.length)
|
|
595
|
+
throw Error('allOf members leave no allowed value');
|
|
596
|
+
const properties = {};
|
|
597
|
+
for (const part of all) {
|
|
598
|
+
for (const [key, property] of Object.entries(part.properties ?? {})) {
|
|
599
|
+
const before = Object.hasOwn(properties, key) ? properties[key] : undefined;
|
|
600
|
+
// The same field from two parts must satisfy both, so its rules add up too.
|
|
601
|
+
const value = before === undefined || JSON.stringify(before) === JSON.stringify(property) ? property : { allOf: [before, property] };
|
|
602
|
+
Object.defineProperty(properties, key, { value, enumerable: true, configurable: true, writable: true });
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
const extras = all.map(part => part.additionalProperties).filter(extra => extra !== undefined);
|
|
606
|
+
// Keys that match patternProperties are allowed even on a closed object.
|
|
607
|
+
const closed = extras.includes(false) && !all.some(part => part.patternProperties !== undefined);
|
|
608
|
+
if (closed && all.some(part => part.additionalProperties === false && Object.keys(properties).some(key => !Object.hasOwn(part.properties ?? {}, key)))) {
|
|
609
|
+
throw Error('Extending a closed object with allOf is not supported');
|
|
610
|
+
}
|
|
611
|
+
const items = all.map(part => part.items).filter((item) => item !== undefined);
|
|
612
|
+
const required = [...new Set(all.flatMap(part => part.required ?? []))];
|
|
613
|
+
const annotations = Object.fromEntries(['title', 'description', ...RULE_KEYWORDS]
|
|
614
|
+
.map(key => [key, first(key)]).filter(([, value]) => value !== undefined));
|
|
615
|
+
return {
|
|
616
|
+
...annotations,
|
|
617
|
+
...(types.length && { type: numeric ? 'integer' : types[0] }),
|
|
618
|
+
...(all.some(part => part.nullable === true) && { nullable: true }),
|
|
619
|
+
...(values && { enum: values }),
|
|
620
|
+
...(all.some(part => part.properties !== undefined) && { properties }),
|
|
621
|
+
...(required.length && { required }),
|
|
622
|
+
...(items.length && { items: items.length === 1 ? items[0] : { allOf: items } }),
|
|
623
|
+
...(extras.length && { additionalProperties: closed ? false : extras.find(extra => typeof extra === 'object') ?? true }),
|
|
624
|
+
// A read-only member still makes the composed property response-only.
|
|
625
|
+
...(all.some(part => part.readOnly === true) && { readOnly: true }),
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
/** oneOf / anyOf: one shape that admits every alternative, as closely as a tool
|
|
629
|
+
* parameter can say it, plus the alternatives in words for the model. The API
|
|
630
|
+
* then refuses a value that fits none of them, as it would from any caller. */
|
|
631
|
+
function joinAlternatives(raw, ctx, where, nesting) {
|
|
632
|
+
if (!raw.length)
|
|
633
|
+
throw Error('oneOf and anyOf need at least one alternative');
|
|
634
|
+
const resolved = raw.map(alternative => resolveOpenApiSchema(alternative, ctx, where, nesting));
|
|
635
|
+
const onlyNull = (s) => s.type === 'null' || (!!s.enum?.length && s.enum.every(v => v === null));
|
|
636
|
+
const options = resolved.flatMap((schema, i) => onlyNull(schema) ? [] : [{ schema, raw: raw[i] }]);
|
|
637
|
+
if (!options.length)
|
|
638
|
+
throw Error('An alternative list that allows only null is not supported');
|
|
639
|
+
const nullable = options.length < resolved.length || options.some(option => option.schema.nullable === true);
|
|
640
|
+
const withNull = (schema) => nullable ? { ...schema, nullable: true } : schema;
|
|
641
|
+
// "X or null" is how most generators write an optional field.
|
|
642
|
+
if (options.length === 1)
|
|
643
|
+
return { schema: withNull(options[0].schema) };
|
|
644
|
+
const labels = options.map(option => alternativeLabel(option.raw, option.schema));
|
|
645
|
+
const types = [...new Set(options.map(option => typeOf(option.schema)).filter((t) => t !== undefined))];
|
|
646
|
+
const type = types.length <= 1 ? types[0]
|
|
647
|
+
: types.every(t => t === 'number' || t === 'integer') ? 'number'
|
|
648
|
+
// A string takes what any of these primitives would say, and on a query or
|
|
649
|
+
// path it is exactly what goes on the wire.
|
|
650
|
+
: types.includes('string') && types.every(t => PRIMITIVE_TYPES.has(t)) ? 'string'
|
|
651
|
+
: undefined;
|
|
652
|
+
if (types.length > 1 && type === undefined) {
|
|
653
|
+
// No one parameter takes both an object and a list. Every alternative is
|
|
654
|
+
// valid by the document's own account, so the first one is kept.
|
|
655
|
+
ctx.notes.add(`"${where}" accepts ${[...new Set(labels)].join(' or ')}. A tool parameter takes one shape, so it ` +
|
|
656
|
+
`sends ${labels[0]}; edit the tool if the agent needs another.`);
|
|
657
|
+
return { schema: withNull(options[0].schema) };
|
|
658
|
+
}
|
|
659
|
+
const all = options.map(option => option.schema);
|
|
660
|
+
const shared = new Map();
|
|
661
|
+
for (const schema of all) {
|
|
662
|
+
for (const [key, property] of Object.entries(schema.properties ?? {})) {
|
|
663
|
+
const seen = shared.get(key) ?? [];
|
|
664
|
+
if (!seen.some(s => JSON.stringify(s) === JSON.stringify(property)))
|
|
665
|
+
seen.push(property);
|
|
666
|
+
shared.set(key, seen);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
const properties = {};
|
|
670
|
+
for (const [key, schemas] of shared) {
|
|
671
|
+
// A field several alternatives name may take any of their shapes.
|
|
672
|
+
Object.defineProperty(properties, key, { value: schemas.length === 1 ? schemas[0] : { anyOf: schemas }, enumerable: true, configurable: true, writable: true });
|
|
673
|
+
}
|
|
674
|
+
const required = all.map(schema => schema.required ?? []).reduce((kept, r) => kept.filter(key => r.includes(key)));
|
|
675
|
+
// Choices only when every alternative lists them, and only in one type.
|
|
676
|
+
const values = all.every(schema => schema.enum) && (types.length <= 1 || type === 'number')
|
|
677
|
+
? [...new Set(all.flatMap(schema => schema.enum))] : undefined;
|
|
678
|
+
const items = all.map(schema => schema.items).filter((item) => item !== undefined);
|
|
679
|
+
const named = options.some(option => option.raw.$ref !== undefined || option.schema.title !== undefined);
|
|
680
|
+
const said = new Set(labels).size > 1 && !(values && !named);
|
|
681
|
+
return {
|
|
682
|
+
schema: withNull({
|
|
683
|
+
...(type && { type }),
|
|
684
|
+
...(shared.size && { properties }),
|
|
685
|
+
...(required.length && { required }),
|
|
686
|
+
...(values && { enum: values }),
|
|
687
|
+
...(items.length && { items: items.length === 1 ? items[0] : { anyOf: items } }),
|
|
688
|
+
...(all.every(schema => schema.additionalProperties === false) && { additionalProperties: false }),
|
|
689
|
+
}),
|
|
690
|
+
...(said && { text: `One of: ${labels.join('; ')}.` }),
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
/** One alternative in a few words: its name, then its fields or its rules. */
|
|
694
|
+
function alternativeLabel(raw, s) {
|
|
695
|
+
const name = typeof raw.$ref === 'string' ? refName(raw.$ref) : typeof s.title === 'string' ? s.title : undefined;
|
|
696
|
+
const type = typeOf(s);
|
|
697
|
+
const fields = s.required?.length ? s.required : Object.keys(s.properties ?? {});
|
|
698
|
+
const shown = `with ${[...fields.slice(0, 8), ...(fields.length > 8 ? ['…'] : [])].join(', ')}`;
|
|
699
|
+
// A fragment that only says which fields are required reads as "with email".
|
|
700
|
+
const detail = type === 'object' || (type === undefined && fields.length)
|
|
701
|
+
? (!fields.length ? TYPE_PHRASE.object : name || type === undefined ? shown : `${TYPE_PHRASE.object} ${shown}`)
|
|
702
|
+
: [
|
|
703
|
+
s.enum?.length === 1 ? JSON.stringify(s.enum[0]) : s.enum ? `one of ${s.enum.map(v => JSON.stringify(v)).join(', ')}` : TYPE_PHRASE[type ?? ''],
|
|
704
|
+
...ruleText(s, type),
|
|
705
|
+
].filter(Boolean).join(', ');
|
|
706
|
+
return name && detail ? `${name} (${detail})` : name ?? (detail || 'any value');
|
|
707
|
+
}
|
|
708
|
+
/** Swagger 2 writes a non-body parameter's schema on the parameter itself. */
|
|
709
|
+
function swaggerParamSchema(p) {
|
|
710
|
+
const schema = { type: p.type ?? 'string' };
|
|
711
|
+
for (const key of ['enum', 'items', ...RULE_KEYWORDS])
|
|
712
|
+
if (p[key] !== undefined)
|
|
713
|
+
schema[key] = p[key];
|
|
714
|
+
return schema;
|
|
715
|
+
}
|
|
716
|
+
/** A tool parameter from a schema. What a parameter cannot carry, a rule or a
|
|
717
|
+
* choice between shapes, is put into its description; a shape no parameter can
|
|
718
|
+
* take at all (a tuple, a recursive tree) still skips the operation, with a
|
|
719
|
+
* warning, rather than drafting a call that cannot be made. */
|
|
720
|
+
function openApiParam(raw, ctx, depth = 0, where = 'body') {
|
|
721
|
+
if (depth > 12)
|
|
722
|
+
throw Error('OpenAPI schema is recursive or exceeds 12 levels');
|
|
723
|
+
const s = resolveOpenApiSchema(raw, ctx, where);
|
|
724
|
+
const t = typeOf(s);
|
|
725
|
+
if (t === undefined || !PARAM_TYPES.has(t))
|
|
726
|
+
throw Error('JSON schema must declare one supported type (optionally nullable)');
|
|
727
|
+
const type = (t === 'integer' ? 'number' : t);
|
|
728
|
+
// Choices the API would refuse, a value of another type, are described instead.
|
|
729
|
+
const fits = (v) => v === null || (t === 'integer' ? Number.isInteger(v) : typeof v === type);
|
|
730
|
+
const choices = s.enum?.length && PRIMITIVE_TYPES.has(t) && s.enum.every(fits) ? s.enum : undefined;
|
|
731
|
+
const rules = ruleText(s, t);
|
|
732
|
+
if (s.enum?.length && !choices)
|
|
733
|
+
rules.push(`one of ${s.enum.map(v => JSON.stringify(v)).join(', ')}`);
|
|
734
|
+
const description = sentences(s.description, rules.length ? `Rules: ${rules.join('; ')}.` : undefined);
|
|
735
|
+
const nullable = s.nullable === true || (choices?.includes(null) ?? false);
|
|
736
|
+
const param = { type, ...(t === 'integer' && { integer: true }), ...(description && { description }), ...(choices && { choices }), ...(nullable && { nullable: true }) };
|
|
737
|
+
if (t === 'object') {
|
|
738
|
+
if (Object.hasOwn(s.properties ?? {}, '__proto__'))
|
|
739
|
+
throw Error('Object property __proto__ is not supported');
|
|
740
|
+
// A map (`additionalProperties` holding a schema) takes any key; its rule is described.
|
|
741
|
+
if (s.additionalProperties !== undefined)
|
|
742
|
+
param.additionalProperties = s.additionalProperties !== false;
|
|
743
|
+
param.properties = Object.fromEntries(Object.entries(s.properties ?? {})
|
|
744
|
+
.filter(([, value]) => value.readOnly !== true)
|
|
745
|
+
.map(([key, value]) => [key, resolveOpenApiSchema(value, ctx, `${where}.${key}`)])
|
|
746
|
+
.filter(([, value]) => value.readOnly !== true)
|
|
747
|
+
.map(([key, value]) => [key, { ...openApiParam(value, ctx, depth + 1, `${where}.${key}`), ...(s.required?.includes(key) && { required: true }) }]));
|
|
748
|
+
for (const key of s.required ?? [])
|
|
749
|
+
if (!Object.hasOwn(s.properties ?? {}, key))
|
|
750
|
+
throw Error(`Required property ${key} has no schema`);
|
|
751
|
+
}
|
|
752
|
+
if (t === 'array') {
|
|
753
|
+
if (!s.items)
|
|
754
|
+
throw Error('Array schema needs items');
|
|
755
|
+
param.items = openApiParam(s.items, ctx, depth + 1, `${where}[]`);
|
|
756
|
+
}
|
|
757
|
+
return param;
|
|
758
|
+
}
|
|
759
|
+
/** Headers are case-insensitive, while query parameter names are not. */
|
|
760
|
+
function securityDestination(scheme) {
|
|
761
|
+
if (scheme.type === 'apiKey' && scheme.in === 'query')
|
|
762
|
+
return `query:${scheme.name}`;
|
|
763
|
+
return `header:${(scheme.type === 'apiKey' ? scheme.name : 'Authorization').toLowerCase()}`;
|
|
764
|
+
}
|
|
765
|
+
function supportedSecurityScheme(scheme) {
|
|
766
|
+
return !!scheme && (scheme.type === 'basic' ||
|
|
767
|
+
(scheme.type === 'http' && ['bearer', 'basic'].includes(scheme.scheme ?? '')) ||
|
|
768
|
+
(scheme.type === 'apiKey' && ['header', 'query'].includes(scheme.in ?? '') && !!scheme.name));
|
|
769
|
+
}
|
|
770
|
+
function selectSecurity(security, schemes, options, parameters) {
|
|
771
|
+
const headerCredentials = new Set();
|
|
772
|
+
for (const p of parameters) {
|
|
773
|
+
if (p.in === 'header' && p.name && CREDENTIAL_HEADERS.has(p.name.toLowerCase())) {
|
|
774
|
+
headerCredentials.add(`header:${p.name.toLowerCase()}`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
let reason = 'No supported security alternative; cookie auth, OAuth acquisition and refresh require explicit configuration';
|
|
778
|
+
// Public operations can still declare credential headers as parameters.
|
|
779
|
+
for (const [index, requirement] of (security.length ? security : [{}]).entries()) {
|
|
780
|
+
const keys = Object.keys(requirement);
|
|
781
|
+
if (!keys.every(key => supportedSecurityScheme(schemes[key])))
|
|
782
|
+
continue;
|
|
783
|
+
if (keys.length > 1 && options.secretName) {
|
|
784
|
+
reason = 'One secretName cannot represent an AND requirement with multiple credentials';
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
const destinations = keys.map(key => securityDestination(schemes[key]));
|
|
788
|
+
if (new Set(destinations).size !== destinations.length) {
|
|
789
|
+
reason = 'Security requirements conflict on the same header or query parameter';
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
// Check parameter credentials against each alternative before selecting it.
|
|
793
|
+
if (options.secretName && new Set([...headerCredentials, ...destinations]).size > 1) {
|
|
794
|
+
reason = 'One secretName cannot represent multiple credentials in one operation';
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
return security.length ? index : -1;
|
|
798
|
+
}
|
|
799
|
+
throw Error(reason);
|
|
800
|
+
}
|
|
801
|
+
/** A document can give different schemes names that normalize identically.
|
|
802
|
+
* Allocate per identity and reserve suffix space before truncating. */
|
|
803
|
+
function openApiSecretNames(schemes, options) {
|
|
804
|
+
const names = new Map();
|
|
805
|
+
const used = new Set();
|
|
806
|
+
const allocate = (identity, wanted) => {
|
|
807
|
+
const existing = names.get(identity);
|
|
808
|
+
if (existing)
|
|
809
|
+
return existing;
|
|
810
|
+
const normalized = normalizeSecretName(options.secretName || wanted);
|
|
811
|
+
let name = normalized;
|
|
812
|
+
if (!options.secretName) {
|
|
813
|
+
for (let index = 2; used.has(name); index++) {
|
|
814
|
+
const suffix = `_${index}`;
|
|
815
|
+
name = normalized.slice(0, 64 - suffix.length) + suffix;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
names.set(identity, name);
|
|
819
|
+
used.add(name);
|
|
820
|
+
return name;
|
|
821
|
+
};
|
|
822
|
+
const defaults = Object.entries(schemes).filter(([, scheme]) => supportedSecurityScheme(scheme))
|
|
823
|
+
.map(([key, scheme]) => [key, secretNameFor(scheme.type === 'apiKey' ? scheme.name : 'Authorization', {})]);
|
|
824
|
+
for (const [key, wanted] of defaults) {
|
|
825
|
+
allocate(`scheme:${key}`, defaults.filter(([, name]) => name === wanted).length > 1 ? key : wanted);
|
|
826
|
+
}
|
|
827
|
+
return allocate;
|
|
828
|
+
}
|
|
829
|
+
/** The security for an operation the document says nothing about, when it
|
|
830
|
+
* defines a scheme and no `security` array anywhere names one.
|
|
443
831
|
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
|
|
832
|
+
* REAL DOCUMENTS DO THIS ALL THE TIME: the scheme is declared under components
|
|
833
|
+
* and the `security` array is never written. Read literally, every operation is
|
|
834
|
+
* then public, which is how drafted tools came out with no credential and failed
|
|
835
|
+
* every call with a 401 nobody had been warned about. One scheme is unambiguous,
|
|
836
|
+
* so it applies, and the developer is told. With several, choosing would be a
|
|
837
|
+
* guess, so none applies and the warning says so. An operation that writes
|
|
838
|
+
* `security: []` stays public: that was said on purpose. */
|
|
839
|
+
function unlistedSecurity(doc, schemes, draft) {
|
|
840
|
+
const names = Object.keys(schemes);
|
|
841
|
+
const operations = Object.values(doc.paths ?? {}).flatMap(item => OPENAPI_METHODS.flatMap(method => item?.[method] ?? []));
|
|
842
|
+
const named = [doc.security, ...operations.map(op => op.security)].some(list => Array.isArray(list) && list.some(requirement => !!requirement && Object.keys(requirement).length > 0));
|
|
843
|
+
// Only an operation the document says nothing about takes the default.
|
|
844
|
+
const silent = doc.security === undefined && operations.some(op => op.security === undefined);
|
|
845
|
+
if (!names.length || named || !silent)
|
|
846
|
+
return [];
|
|
847
|
+
const only = names.length === 1 ? names[0] : undefined;
|
|
848
|
+
if (only !== undefined && supportedSecurityScheme(schemes[only])) {
|
|
849
|
+
draft.warnings.push({ kind: 'security-choice', message: `The document defines one security scheme, "${only}", and no operation lists it under "security", ` +
|
|
850
|
+
`so every tool whose operation does not set "security" sends it. Remove it from any tool that calls ` +
|
|
851
|
+
`a public endpoint.` });
|
|
852
|
+
return [{ [only]: [] }];
|
|
853
|
+
}
|
|
854
|
+
draft.warnings.push({ kind: 'security-choice', message: only !== undefined
|
|
855
|
+
? `The document defines the security scheme "${only}" (${schemes[only]?.type ?? 'no type'}), which the import cannot set up, ` +
|
|
856
|
+
`and no operation lists it, so the tools send no credential. Add the header the API expects to each tool.`
|
|
857
|
+
: `The document defines ${names.length} security schemes (${names.map(n => `"${n}"`).join(', ')}) and no operation lists one ` +
|
|
858
|
+
`under "security", so the tools send no credential. List the one the API uses under "security" and import again, ` +
|
|
859
|
+
`or add the header to each tool.` });
|
|
860
|
+
return [];
|
|
861
|
+
}
|
|
862
|
+
/** Draft supported OpenAPI/Swagger requests; authentication follows each
|
|
863
|
+
* operation's security requirement, including AND, alternatives and security:[]. */
|
|
447
864
|
export function fromOpenApi(doc, options = {}) {
|
|
448
865
|
const draft = new Draft();
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
(
|
|
452
|
-
if (!base) {
|
|
453
|
-
draft.warnings.push({
|
|
454
|
-
kind: 'undefined-variable',
|
|
455
|
-
message: 'The document names no server, so every url is a path with no host. Set one, or edit ' +
|
|
456
|
-
'the urls before creating the agent: the API refuses a url that is not absolute.',
|
|
457
|
-
});
|
|
458
|
-
}
|
|
866
|
+
const base = doc.servers?.[0]?.url ?? (doc.host ? `${doc.schemes?.[0] ?? 'https'}://${doc.host}${doc.basePath ?? ''}` : '');
|
|
867
|
+
if (!base)
|
|
868
|
+
draft.warnings.push({ kind: 'undefined-variable', message: 'The document names no server. Set absolute HTTP(S) tool URLs before creating the agent.' });
|
|
459
869
|
const schemes = doc.components?.securitySchemes ?? doc.securityDefinitions ?? {};
|
|
460
|
-
const
|
|
461
|
-
|
|
870
|
+
const secretName = openApiSecretNames(schemes, options);
|
|
871
|
+
const unlisted = unlistedSecurity(doc, schemes, draft);
|
|
872
|
+
for (const [path, item] of Object.entries(doc.paths ?? {}))
|
|
462
873
|
for (const method of OPENAPI_METHODS) {
|
|
463
|
-
const op =
|
|
874
|
+
const op = item[method];
|
|
464
875
|
if (!op)
|
|
465
876
|
continue;
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
if (options.include && !options.include({ name: candidateName, method: method.toUpperCase(), url: fullUrl })) {
|
|
877
|
+
const candidate = toolNameFrom(op.operationId ?? `${method}_${path}`, 'op');
|
|
878
|
+
if (options.include && !options.include({ name: candidate, method: method.toUpperCase(), url: base + path }))
|
|
469
879
|
continue;
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
// parameter would otherwise leave `{{params.order-id}}` in the url.
|
|
478
|
-
let url = `${base}${path}`.replace(/\{([^}]+)\}/g, (_m, p) => `{{params.${draft.param(params, p, { type: 'string', required: true }, name)}}}`);
|
|
479
|
-
const queryPairs = [];
|
|
480
|
-
for (const p of op.parameters ?? []) {
|
|
481
|
-
if (!p.name)
|
|
482
|
-
continue;
|
|
483
|
-
const def = {
|
|
484
|
-
type: openApiType(p.schema?.type),
|
|
485
|
-
...(p.description ? { description: p.description } : {}),
|
|
486
|
-
...(p.required ? { required: true } : {}),
|
|
487
|
-
...(p.schema?.enum ? { choices: p.schema.enum } : {}),
|
|
488
|
-
};
|
|
489
|
-
// A CREDENTIAL HEADER IS NEVER A PARAM. The model would have to be
|
|
490
|
-
// handed the token to fill it in, and the whole point of the secret
|
|
491
|
-
// placeholder is that it never sees one.
|
|
492
|
-
if (p.in === 'header' && CREDENTIAL_HEADERS.has(p.name.toLowerCase())) {
|
|
493
|
-
const secret = secretNameFor(p.name, options);
|
|
494
|
-
headerPairs[p.name] = `{{secrets.${secret}}}`;
|
|
495
|
-
draft.secret(secret, name, `securityScheme (header parameter "${p.name}")`);
|
|
496
|
-
continue;
|
|
880
|
+
const name = draft.uniqueName(candidate);
|
|
881
|
+
try {
|
|
882
|
+
const ctx = { doc, notes: new Set() };
|
|
883
|
+
const merged = new Map();
|
|
884
|
+
for (const raw of [...(item.parameters ?? []), ...(op.parameters ?? [])]) {
|
|
885
|
+
const p = dereference(raw, doc);
|
|
886
|
+
merged.set(p.in + ':' + p.name, p);
|
|
497
887
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
888
|
+
const security = op.security ?? doc.security ?? unlisted;
|
|
889
|
+
const selected = selectSecurity(security, schemes, options, merged.values());
|
|
890
|
+
const keys = Object.keys(security[selected] ?? {});
|
|
891
|
+
const credentialDestinations = new Set(keys.map(key => securityDestination(schemes[key])));
|
|
892
|
+
const params = {}, headers = {}, query = [];
|
|
893
|
+
let url = (base + path).replace(/\{([^}]+)\}/g, (_match, p) => `{{paramsEncoded.${draft.param(params, p, { type: 'string', required: true }, name, 'path:' + p)}}}`);
|
|
894
|
+
let body;
|
|
895
|
+
for (const p of merged.values()) {
|
|
896
|
+
if (!p.name)
|
|
897
|
+
throw Error('Parameter has no name');
|
|
898
|
+
// A declared auth parameter and a security scheme describe the same
|
|
899
|
+
// wire credential. Emit it once, as a secret, never as model input.
|
|
900
|
+
const destination = `${p.in}:${p.in === 'header' ? p.name.toLowerCase() : p.name}`;
|
|
901
|
+
if (credentialDestinations.has(destination))
|
|
902
|
+
continue;
|
|
903
|
+
if (p.in === 'cookie') {
|
|
904
|
+
if (p.required)
|
|
905
|
+
throw Error('Required cookie parameters are not supported');
|
|
906
|
+
draft.warnings.push({ kind: 'unsupported-body', tool: name, message: `Optional cookie parameter "${p.name}" is not supported; it was omitted.` });
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
if (!['path', 'query', 'header', 'body'].includes(p.in ?? ''))
|
|
910
|
+
throw Error(`Unsupported parameter location ${p.in}`);
|
|
911
|
+
const schema = p.schema ?? swaggerParamSchema(p);
|
|
912
|
+
// The parameter's own description leads, and the schema's rules follow it.
|
|
913
|
+
const def = openApiParam(p.description ? { ...schema, description: p.description } : schema, ctx, 0, p.name);
|
|
914
|
+
if (p.required || p.in === 'path')
|
|
915
|
+
def.required = true;
|
|
916
|
+
if (p.in !== 'body' && (['object', 'array'].includes(def.type) || (p.style && !['simple', 'form'].includes(p.style))))
|
|
917
|
+
throw Error('Structured parameter serialization is unsupported outside a JSON body');
|
|
918
|
+
if (p.in === 'header' && CREDENTIAL_HEADERS.has(p.name.toLowerCase())) {
|
|
919
|
+
const secret = secretName(`header:${p.name.toLowerCase()}`, secretNameFor(p.name, {}));
|
|
920
|
+
headers[p.name] = `{{secrets.${secret}}}`;
|
|
921
|
+
draft.secret(secret, name, `header parameter ${p.name}`);
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
const param = draft.param(params, p.name, def, name, p.in + ':' + p.name);
|
|
925
|
+
if (p.in === 'query')
|
|
926
|
+
query.push(`${encodeURIComponent(p.name)}={{paramsEncoded.${param}}}`);
|
|
927
|
+
if (p.in === 'header')
|
|
928
|
+
headers[p.name] = `{{params.${param}}}`;
|
|
929
|
+
if (p.in === 'body')
|
|
930
|
+
body = `{{paramsJson.${param}}}`;
|
|
508
931
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
932
|
+
if (op.requestBody && method !== 'get') {
|
|
933
|
+
const requestBody = dereference(op.requestBody, doc);
|
|
934
|
+
const media = Object.entries(requestBody.content ?? {}).find(([type]) => /(?:\/json|\+json)$/.test(type));
|
|
935
|
+
if (!media?.[1].schema)
|
|
936
|
+
throw Error('Only a JSON request body with a declared schema is supported');
|
|
937
|
+
const def = openApiParam(media[1].schema, ctx);
|
|
938
|
+
if (requestBody.required)
|
|
939
|
+
def.required = true;
|
|
940
|
+
const param = draft.param(params, 'body', def, name, 'requestBody');
|
|
941
|
+
body = `{{paramsJson.${param}}}`;
|
|
942
|
+
headers['Content-Type'] = media[0];
|
|
512
943
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
944
|
+
if (security.length > 1)
|
|
945
|
+
draft.warnings.push({ kind: 'security-choice', tool: name, message: `Selected security alternative ${selected + 1}; review the choice before creating the agent.` });
|
|
946
|
+
for (const key of keys) {
|
|
947
|
+
const scheme = schemes[key];
|
|
948
|
+
const header = scheme.type === 'apiKey' ? scheme.name : 'Authorization';
|
|
949
|
+
const secret = secretName(`scheme:${key}`, header);
|
|
950
|
+
if (scheme.type === 'apiKey' && scheme.in === 'query')
|
|
951
|
+
query.push(`${encodeURIComponent(header)}={{secretsEncoded.${secret}}}`);
|
|
952
|
+
else {
|
|
953
|
+
headers[header] = scheme.type === 'apiKey' ? `{{secrets.${secret}}}` : `${scheme.type === 'basic' || scheme.scheme === 'basic' ? 'Basic' : 'Bearer'} {{secrets.${secret}}}`;
|
|
954
|
+
}
|
|
955
|
+
draft.secret(secret, name, `securityScheme ${key}`);
|
|
520
956
|
}
|
|
957
|
+
if (query.length)
|
|
958
|
+
url += '?' + query.join('&');
|
|
959
|
+
draft.tools.push({ name, description: describe([op.description, op.summary], name, method.toUpperCase(), path, draft), method: method.toUpperCase(), url, ...(Object.keys(headers).length && { headers }), ...(Object.keys(params).length && { params }), ...(draft.secretsFor(name).length && { secrets: draft.secretsFor(name) }), ...(body !== undefined && method !== 'get' && { body }) });
|
|
960
|
+
for (const note of ctx.notes)
|
|
961
|
+
draft.warnings.push({ kind: 'unsupported-body', tool: name, message: note });
|
|
521
962
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
if (firstScheme) {
|
|
525
|
-
const secret = secretNameFor(firstScheme.in === 'header' && firstScheme.name ? firstScheme.name : 'authorization', options);
|
|
526
|
-
const headerName = firstScheme.type === 'apiKey' && firstScheme.in === 'header' && firstScheme.name
|
|
527
|
-
? firstScheme.name
|
|
528
|
-
: 'Authorization';
|
|
529
|
-
headerPairs[headerName] =
|
|
530
|
-
headerName.toLowerCase() === 'authorization'
|
|
531
|
-
? `${firstScheme.scheme === 'basic' ? 'Basic' : 'Bearer'} {{secrets.${secret}}}`
|
|
532
|
-
: `{{secrets.${secret}}}`;
|
|
533
|
-
draft.secret(secret, name, `securityScheme (${firstScheme.type ?? 'unknown'})`);
|
|
963
|
+
catch (error) {
|
|
964
|
+
draft.warnings.push({ kind: 'unsupported-operation', tool: name, message: `Skipped ${method.toUpperCase()} ${path}: ${error.message}. Configure this operation explicitly.` });
|
|
534
965
|
}
|
|
535
|
-
const headers = safeHeaders(headerPairs, name, draft, options, 'OpenAPI');
|
|
536
|
-
const upper = method.toUpperCase();
|
|
537
|
-
let body;
|
|
538
|
-
if (op.requestBody && upper !== 'GET') {
|
|
539
|
-
const types = Object.keys(op.requestBody.content ?? {});
|
|
540
|
-
if (types.some((t) => t.includes('json'))) {
|
|
541
|
-
// A LITERAL EXAMPLE IS NOT INVENTED HERE. The schema says what the
|
|
542
|
-
// shape is; guessing values would produce a tool that posts
|
|
543
|
-
// plausible nonsense. The agent fills this in, so it is left for
|
|
544
|
-
// the developer to shape.
|
|
545
|
-
body = '{{paramsJson.body}}';
|
|
546
|
-
params.body = { type: 'string', description: 'JSON body for this request.', required: true };
|
|
547
|
-
}
|
|
548
|
-
else if (types.length > 0) {
|
|
549
|
-
draft.warnings.push({
|
|
550
|
-
kind: 'unsupported-body',
|
|
551
|
-
tool: name,
|
|
552
|
-
message: `Request body is ${types.join(', ')}, which is not supported; the tool was created without one.`,
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
draft.tools.push({
|
|
557
|
-
name,
|
|
558
|
-
description: describe([op.description, op.summary], name, upper, path, draft),
|
|
559
|
-
method: upper,
|
|
560
|
-
url,
|
|
561
|
-
...(Object.keys(headers).length > 0 && { headers }),
|
|
562
|
-
...(Object.keys(params).length > 0 && { params }),
|
|
563
|
-
...(draft.secretsFor(name).length > 0 && { secrets: draft.secretsFor(name) }),
|
|
564
|
-
...(body !== undefined && { body }),
|
|
565
|
-
});
|
|
566
966
|
}
|
|
567
|
-
|
|
568
|
-
|
|
967
|
+
const result = draft.finish(options);
|
|
968
|
+
const emitted = new Set(result.tools.map(t => t.name));
|
|
969
|
+
result.secrets = result.secrets.map(s => ({ ...s, usedBy: s.usedBy.filter(name => emitted.has(name)) })).filter(s => s.usedBy.length > 0);
|
|
970
|
+
return result;
|
|
569
971
|
}
|
|
570
972
|
/**
|
|
571
973
|
* Fetch an OpenAPI document and draft tools from it.
|