@cerebro-labs/agentnava-sdk 0.26.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.
Files changed (67) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +12 -0
  3. package/README.md +230 -0
  4. package/agent.d.ts +285 -0
  5. package/agent.js +394 -0
  6. package/agent.js.map +1 -0
  7. package/attach.d.ts +49 -0
  8. package/attach.js +115 -0
  9. package/attach.js.map +1 -0
  10. package/connection-status.d.ts +28 -0
  11. package/connection-status.js +61 -0
  12. package/connection-status.js.map +1 -0
  13. package/connection.d.ts +89 -0
  14. package/connection.js +143 -0
  15. package/connection.js.map +1 -0
  16. package/conversation.d.ts +177 -0
  17. package/conversation.js +348 -0
  18. package/conversation.js.map +1 -0
  19. package/end-user.d.ts +159 -0
  20. package/end-user.js +209 -0
  21. package/end-user.js.map +1 -0
  22. package/errors.d.ts +43 -0
  23. package/errors.js +55 -0
  24. package/errors.js.map +1 -0
  25. package/http.d.ts +32 -0
  26. package/http.js +192 -0
  27. package/http.js.map +1 -0
  28. package/import-tools.d.ts +282 -0
  29. package/import-tools.js +686 -0
  30. package/import-tools.js.map +1 -0
  31. package/index.d.ts +16 -0
  32. package/index.js +38 -0
  33. package/index.js.map +1 -0
  34. package/knowledge-base.d.ts +165 -0
  35. package/knowledge-base.js +274 -0
  36. package/knowledge-base.js.map +1 -0
  37. package/load-agent-dir.d.ts +17 -0
  38. package/load-agent-dir.js +192 -0
  39. package/load-agent-dir.js.map +1 -0
  40. package/package.json +32 -0
  41. package/project.d.ts +94 -0
  42. package/project.js +155 -0
  43. package/project.js.map +1 -0
  44. package/secret.d.ts +45 -0
  45. package/secret.js +69 -0
  46. package/secret.js.map +1 -0
  47. package/templates-inline.d.ts +2 -0
  48. package/templates-inline.js +16 -0
  49. package/templates-inline.js.map +1 -0
  50. package/templates.d.ts +16 -0
  51. package/templates.js +32 -0
  52. package/templates.js.map +1 -0
  53. package/turn.d.ts +11 -0
  54. package/turn.js +244 -0
  55. package/turn.js.map +1 -0
  56. package/types.d.ts +485 -0
  57. package/types.js +4 -0
  58. package/types.js.map +1 -0
  59. package/version.d.ts +1 -0
  60. package/version.js +5 -0
  61. package/version.js.map +1 -0
  62. package/workflows.d.ts +36 -0
  63. package/workflows.js +101 -0
  64. package/workflows.js.map +1 -0
  65. package/workspace.d.ts +290 -0
  66. package/workspace.js +390 -0
  67. package/workspace.js.map +1 -0
@@ -0,0 +1,686 @@
1
+ // Turn an API you already describe somewhere into tools an agent can call.
2
+ //
3
+ // WHAT THIS IS FOR. A company adopting AgentNava has APIs already, and their
4
+ // team already keeps a Postman collection, an OpenAPI document, or a handful of
5
+ // curl commands that work. Asking them to hand-write a tool declaration per
6
+ // endpoint is asking for a week of somebody's time, and asking them to produce
7
+ // an artifact they do not already have is worse: it is a reason to stall.
8
+ //
9
+ // So this takes whichever of those three they have and gives back draft
10
+ // `httpTools`. Self-serve, in the SDK, no support ticket.
11
+ //
12
+ // IT RUNS ENTIRELY ON YOUR MACHINE. Nothing here makes a network request except
13
+ // `fromOpenApi` when you hand it a URL instead of a document, and that fetch
14
+ // goes to YOUR server, not ours. That is a deliberate security property rather
15
+ // than an implementation convenience: a Postman export routinely contains a live
16
+ // token, and a collection you never upload is a token we can never receive.
17
+ //
18
+ // WHAT IT DOES NOT DO, and this is the honest part:
19
+ //
20
+ // IT CANNOT WRITE THE DESCRIPTIONS. A tool's `description` is what the model
21
+ // reads to decide "this is the tool for this job", and none of these three
22
+ // formats contains that. Their summaries are written for a developer reading
23
+ // documentation, not for a model choosing between forty tools. "Fetch one
24
+ // order by its number, before deciding a refund" is what works; "Orders
25
+ // endpoint" is not. Where the source has nothing useful, the tool comes back
26
+ // with a warning rather than a confident-looking stub, because a bad
27
+ // description does not fail: it quietly makes the agent pick the wrong tool.
28
+ //
29
+ // IT DOES NOT CREATE ANYTHING. It returns drafts. You read them, fix the
30
+ // descriptions, drop the endpoints an agent has no business calling, and pass
31
+ // what is left to `ws.agents.create({ httpTools })`.
32
+ //
33
+ // ```ts
34
+ // import { fromPostman } from '@cerebro-labs/agentnava-sdk';
35
+ //
36
+ // const { tools, secrets, warnings } = fromPostman(collection);
37
+ // for (const w of warnings) console.warn(w.message);
38
+ // for (const s of secrets) await ws.secrets.create({ name: s.name, value: '...' });
39
+ //
40
+ // await ws.agents.create({ id: 'support', instructions: '...', httpTools: tools });
41
+ // ```
42
+ const DEFAULT_WARN_ABOVE = 15;
43
+ /** Header names whose value is a credential whatever it looks like.
44
+ *
45
+ * MATCHED ON THE NAME, NOT THE VALUE, and that is the safe direction. A value
46
+ * test has to guess, and the cost of guessing wrong is a live token written
47
+ * into an agent version, which is readable. A name test over-collects at worst:
48
+ * it turns something harmless into a secret, which is an inconvenience. */
49
+ const CREDENTIAL_HEADERS = new Set([
50
+ 'authorization',
51
+ 'proxy-authorization',
52
+ 'cookie',
53
+ 'x-api-key',
54
+ 'api-key',
55
+ 'apikey',
56
+ 'x-auth-token',
57
+ 'auth-token',
58
+ 'x-access-token',
59
+ 'access-token',
60
+ 'token',
61
+ 'x-session-token',
62
+ 'private-token',
63
+ 'x-secret',
64
+ ]);
65
+ /** Shapes that are a credential wherever they appear, including in a URL.
66
+ *
67
+ * Deliberately narrow: each is a published prefix from a real provider, so a
68
+ * match is near-certain rather than a heuristic. The header rule above is what
69
+ * does the heavy lifting. */
70
+ const CREDENTIAL_VALUE_RE = /\b(sk-[A-Za-z0-9]{16,}|ghp_[A-Za-z0-9]{20,}|gho_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|AIza[A-Za-z0-9_-]{30,}|AKIA[A-Z0-9]{16}|eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.)/;
71
+ /** A tool name the API will accept: `^[a-z][a-z0-9_]{0,63}$`. */
72
+ export function toolNameFrom(raw, fallback = 'call') {
73
+ const slug = raw
74
+ .trim()
75
+ .toLowerCase()
76
+ .replace(/[^a-z0-9]+/g, '_')
77
+ .replace(/^_+|_+$/g, '')
78
+ .slice(0, 64);
79
+ if (slug === '' || !/^[a-z]/.test(slug)) {
80
+ const prefixed = `${fallback}_${slug}`.replace(/_+$/, '').slice(0, 64);
81
+ return /^[a-z][a-z0-9_]*$/.test(prefixed) ? prefixed : fallback;
82
+ }
83
+ return slug;
84
+ }
85
+ /** A param name the API will accept: `^[A-Za-z][A-Za-z0-9_]*$`.
86
+ *
87
+ * THE WIRE NAME IS NOT THE PARAM NAME, and conflating them is what broke this.
88
+ * An API's query key is whatever the API says it is (`page-size`,
89
+ * `filter[status]`), while a param name is an identifier both our validator and
90
+ * the runtime's placeholder regex constrain to `[A-Za-z][A-Za-z0-9_]*`. Copying
91
+ * the wire name into the param name produced `400 params has invalid key
92
+ * "page-size"` on documents that are completely ordinary, which is exactly the
93
+ * unactionable 400 `uniqueName` exists to avoid. So the key on the wire keeps
94
+ * its spelling and only the identifier is normalised. */
95
+ export function paramNameFrom(raw, fallback = 'arg') {
96
+ const slug = raw
97
+ .trim()
98
+ .replace(/[^A-Za-z0-9]+/g, '_')
99
+ .replace(/^_+|_+$/g, '')
100
+ .slice(0, 64);
101
+ if (slug === '' || !/^[A-Za-z]/.test(slug)) {
102
+ const prefixed = `${fallback}_${slug}`.replace(/_+$/, '').slice(0, 64);
103
+ return /^[A-Za-z][A-Za-z0-9_]*$/.test(prefixed) ? prefixed : fallback;
104
+ }
105
+ return slug;
106
+ }
107
+ /** A secret name the runtime's placeholder regex will match.
108
+ *
109
+ * `[A-Za-z_][A-Za-z0-9_]*`, so a leading digit is the case that matters. A
110
+ * Postman collection with a `{{2fa_token}}` variable produced
111
+ * `{{secrets.2FA_TOKEN}}`, which that regex does not match at all, so nothing
112
+ * substituted it and the LITERAL braces went out as the header value. No error,
113
+ * just a 401 from the far end. A name that cannot resolve is worse than an ugly
114
+ * one. */
115
+ function normalizeSecretName(raw, fallback = 'API_TOKEN') {
116
+ const slug = raw
117
+ .replace(/[^A-Za-z0-9]+/g, '_')
118
+ .replace(/^_+|_+$/g, '')
119
+ .toUpperCase()
120
+ .slice(0, 64);
121
+ if (slug === '')
122
+ return fallback;
123
+ return /^[A-Za-z_]/.test(slug) ? slug : `S_${slug}`.slice(0, 64);
124
+ }
125
+ /** A secret name from a header, or the caller's own choice. */
126
+ function secretNameFor(header, options) {
127
+ if (options.secretName)
128
+ return options.secretName;
129
+ const h = header.toLowerCase();
130
+ if (h === 'authorization')
131
+ return 'API_TOKEN';
132
+ if (h === 'x-api-key' || h === 'api-key' || h === 'apikey')
133
+ return 'API_KEY';
134
+ return normalizeSecretName(header);
135
+ }
136
+ /** Accumulates tools, secrets and warnings, and keeps names unique.
137
+ *
138
+ * UNIQUE BECAUSE THE API NOW REFUSES DUPLICATES. Two endpoints called "Get" in
139
+ * different folders both slug to `get`, and a duplicate name makes which
140
+ * request goes out a matter of list order: the agent looks like it called the
141
+ * right tool and returns the wrong answer. Renaming here, with a warning, beats
142
+ * a 400 the importer's caller cannot act on. */
143
+ class Draft {
144
+ tools = [];
145
+ warnings = [];
146
+ #secrets = new Map();
147
+ #names = new Set();
148
+ /** `<tool>\0<wire name>` to the param name chosen for it. */
149
+ #paramNames = new Map();
150
+ uniqueName(wanted) {
151
+ if (!this.#names.has(wanted)) {
152
+ this.#names.add(wanted);
153
+ return wanted;
154
+ }
155
+ for (let i = 2; i < 1000; i++) {
156
+ const candidate = `${wanted}_${i}`.slice(0, 64);
157
+ if (!this.#names.has(candidate)) {
158
+ this.#names.add(candidate);
159
+ this.warnings.push({
160
+ kind: 'name-collision',
161
+ tool: candidate,
162
+ message: `Two operations both became "${wanted}"; this one is "${candidate}". Give it a name that says what it does.`,
163
+ });
164
+ return candidate;
165
+ }
166
+ }
167
+ throw new Error(`cannot find a free name for ${wanted}`);
168
+ }
169
+ /** Records the secret. Returns true the FIRST time this name is seen, so the
170
+ * caller can warn once rather than once per tool that uses it. */
171
+ secret(name, tool, from) {
172
+ const existing = this.#secrets.get(name);
173
+ if (existing) {
174
+ if (!existing.usedBy.includes(tool))
175
+ existing.usedBy.push(tool);
176
+ return false;
177
+ }
178
+ this.#secrets.set(name, { name, usedBy: [tool], from });
179
+ return true;
180
+ }
181
+ /** Every secret this tool references, for the tool's own `secrets` field.
182
+ *
183
+ * LOAD-BEARING, NOT DECORATION. `collectSecretNames` in the control plane
184
+ * builds a turn's secret map from each httpTool's `secrets` array; unlike
185
+ * MCP servers it does NOT scan the tool's headers for {{secrets.X}}. A tool
186
+ * that carries the placeholder but not the declaration therefore fails every
187
+ * call with "missing required secret", which is what every drafted tool did
188
+ * until this existed. */
189
+ secretsFor(tool) {
190
+ return [...this.#secrets.values()].filter((s) => s.usedBy.includes(tool)).map((s) => s.name);
191
+ }
192
+ /** Declare a param under a name the API accepts, warning when it had to
193
+ * change, and return the name to use in the placeholder.
194
+ *
195
+ * ONE WIRE NAME IS ONE PARAM, however many times it is mentioned. An
196
+ * OpenAPI operation names a path variable twice: once in the path template
197
+ * `/orders/{order-id}` and once in `parameters`. Treating the second sight
198
+ * of it as a collision produced `order_id` in the url and a second,
199
+ * never-sent `order_id_2` in `params`, which is the very "declared and never
200
+ * sent" defect this class is meant to prevent. So a repeat returns the same
201
+ * name and merges the richer definition over the first guess. */
202
+ param(params, wireName, def, tool) {
203
+ const key = `${tool}\u0000${wireName}`;
204
+ const already = this.#paramNames.get(key);
205
+ if (already !== undefined) {
206
+ params[already] = { ...params[already], ...def };
207
+ return already;
208
+ }
209
+ const wanted = paramNameFrom(wireName);
210
+ let name = wanted;
211
+ for (let i = 2; i < 1000 && name in params; i++)
212
+ name = `${wanted}_${i}`.slice(0, 64);
213
+ this.#paramNames.set(key, name);
214
+ if (name !== wireName) {
215
+ this.warnings.push({
216
+ kind: 'param-renamed',
217
+ tool,
218
+ message: `Parameter "${wireName}" is called "${name}" on the tool, because a param name has ` +
219
+ `to be a plain identifier. The request still sends "${wireName}" on the wire.`,
220
+ });
221
+ }
222
+ params[name] = def;
223
+ return name;
224
+ }
225
+ finish(options) {
226
+ const limit = options.warnAbove ?? DEFAULT_WARN_ABOVE;
227
+ if (this.tools.length > limit) {
228
+ this.warnings.push({
229
+ kind: 'too-many-tools',
230
+ message: `${this.tools.length} tools. A model chooses markedly worse between forty than ` +
231
+ `between eight, so keep the operations an agent genuinely needs and drop the rest ` +
232
+ `(pass \`include\` to filter during the import).`,
233
+ });
234
+ }
235
+ return { tools: this.tools, secrets: [...this.#secrets.values()], warnings: this.warnings };
236
+ }
237
+ }
238
+ /** Headers with every credential replaced by a secret placeholder. */
239
+ function safeHeaders(raw, toolName, draft, options, source) {
240
+ const out = {};
241
+ for (const [name, value] of Object.entries(raw)) {
242
+ const isCredentialHeader = CREDENTIAL_HEADERS.has(name.toLowerCase());
243
+ const looksLikeOne = CREDENTIAL_VALUE_RE.test(value);
244
+ if (!isCredentialHeader && !looksLikeOne) {
245
+ out[name] = value;
246
+ continue;
247
+ }
248
+ const secret = secretNameFor(name, options);
249
+ // The SCHEME is preserved, the credential is not. `Bearer {{secrets.X}}`
250
+ // works; `{{secrets.X}}` alone silently drops the scheme and the server
251
+ // answers 401 about a malformed header.
252
+ const scheme = /^(Bearer|Basic|Token|ApiKey)\s+/i.exec(value)?.[1];
253
+ out[name] = scheme ? `${scheme} {{secrets.${secret}}}` : `{{secrets.${secret}}}`;
254
+ const firstTime = draft.secret(secret, toolName, `${source} header "${name}"`);
255
+ // ONCE PER SECRET, not once per tool. One token shared by twelve
256
+ // operations produced twelve identical warnings, and a list nobody can
257
+ // skim is a list nobody reads. `secrets[].usedBy` already names every tool
258
+ // that references it, so the detail is not lost.
259
+ if (firstTime) {
260
+ draft.warnings.push({
261
+ kind: 'credential-stripped',
262
+ message: `Header "${name}" held a credential. It is now {{secrets.${secret}}} and the value was ` +
263
+ `NOT kept. Create it with ws.secrets.create({ name: '${secret}', value: '…' }).`,
264
+ });
265
+ }
266
+ }
267
+ return out;
268
+ }
269
+ /** A description the model can act on, or a warning that there is none. */
270
+ function describe(candidates, toolName, method, path, draft) {
271
+ const found = candidates.map((c) => c?.trim()).find((c) => c && c.length >= 12);
272
+ if (found)
273
+ return found;
274
+ // The fallback is `METHOD /path`, and the path must be the RESOLVED one. Using
275
+ // the raw source url put `DELETE {{baseUrl}}/admin/purge` in a description,
276
+ // which shows a model a placeholder it cannot act on and reads like a bug.
277
+ // Collapse doubled slashes left behind by a stripped placeholder, but NOT the
278
+ // `//` in `https://`: a Postman collection that writes absolute urls rather
279
+ // than {{baseUrl}} produced the description "DELETE https:/api.acme.example/…",
280
+ // and that string is what the model reads to choose the tool.
281
+ const stripped = path.replace(/\{\{[^}]*\}\}/g, '');
282
+ const scheme = /^[a-z][a-z0-9+.-]*:\/\//i.exec(stripped)?.[0] ?? '';
283
+ const clean = `${scheme}${stripped.slice(scheme.length).replace(/\/{2,}/g, '/')}` || path;
284
+ draft.warnings.push({
285
+ kind: 'description-missing',
286
+ tool: toolName,
287
+ message: `"${toolName}" has no usable description. This is the field the model reads to decide ` +
288
+ `whether to call it, so write one line saying when someone would use it. ` +
289
+ `A bad description does not error, it makes the agent pick the wrong tool.`,
290
+ });
291
+ return `${method} ${clean}`;
292
+ }
293
+ /**
294
+ * Draft tools from a Postman collection export (v2.0 or v2.1).
295
+ *
296
+ * WHY POSTMAN AND NOT ONLY OPENAPI. OpenAPI is tidier; a collection is truer. It
297
+ * holds the requests somebody has actually run, with the header names that
298
+ * really work and real values in the body, which is what a tool has to
299
+ * reproduce. Teams also already have one, and the best format to ask for is the
300
+ * one nobody has to make.
301
+ *
302
+ * WHAT IT COSTS: a collection has no parameter TYPES, so every parameter comes
303
+ * back a string. That is usually right and always safe; `fromOpenApi` gives real
304
+ * types where a document exists.
305
+ *
306
+ * VARIABLES. Postman writes `{{baseUrl}}`, which is the same shape as our own
307
+ * placeholders, so they map across almost directly. A variable the collection
308
+ * DEFINES is substituted, unless it holds something that looks like a
309
+ * credential, in which case it becomes a secret instead of being inlined. An
310
+ * undefined variable becomes a secret too, with a warning, because the
311
+ * alternative is a URL with a placeholder nothing resolves.
312
+ */
313
+ export function fromPostman(collection, options = {}) {
314
+ const draft = new Draft();
315
+ const vars = new Map();
316
+ for (const v of collection.variable ?? []) {
317
+ if (v.key)
318
+ vars.set(v.key, v.value ?? '');
319
+ }
320
+ /** Substitute defined variables; anything else becomes a secret. */
321
+ const resolveVars = (text, toolName) => text.replace(/\{\{([^}]+)\}\}/g, (_m, rawKey) => {
322
+ const keyName = rawKey.trim();
323
+ const value = vars.get(keyName);
324
+ // A DEFINED VARIABLE HOLDING A CREDENTIAL IS STILL A CREDENTIAL. Inlining
325
+ // it because the collection happened to define it is how a live token
326
+ // ends up in an agent version, which is readable.
327
+ if (value !== undefined && value !== '' && !CREDENTIAL_VALUE_RE.test(value)) {
328
+ return value;
329
+ }
330
+ const secret = normalizeSecretName(keyName);
331
+ draft.secret(secret, toolName, `collection variable "${keyName}"`);
332
+ if (value === undefined) {
333
+ draft.warnings.push({
334
+ kind: 'undefined-variable',
335
+ tool: toolName,
336
+ message: `{{${keyName}}} is not defined in the collection, so it became {{secrets.${secret}}}.`,
337
+ });
338
+ }
339
+ return `{{secrets.${secret}}}`;
340
+ });
341
+ const walk = (items, trail) => {
342
+ for (const item of items) {
343
+ if (item.item) {
344
+ walk(item.item, [...trail, item.name ?? '']);
345
+ continue;
346
+ }
347
+ const request = item.request;
348
+ if (!request || typeof request === 'string')
349
+ continue;
350
+ const rawUrl = typeof request.url === 'string'
351
+ ? request.url
352
+ : (request.url?.raw ??
353
+ [
354
+ (request.url?.host ?? []).join('.'),
355
+ (request.url?.path ?? []).join('/'),
356
+ ]
357
+ .filter(Boolean)
358
+ .join('/'));
359
+ if (!rawUrl)
360
+ continue;
361
+ const method = (request.method ?? 'GET').toUpperCase();
362
+ const candidateName = toolNameFrom(item.name ?? `${method} ${rawUrl}`);
363
+ if (options.include && !options.include({ name: candidateName, method, url: rawUrl })) {
364
+ continue;
365
+ }
366
+ const name = draft.uniqueName(candidateName);
367
+ // Postman path variables are `:id`; ours are `{{params.id}}`.
368
+ const params = {};
369
+ let url = resolveVars(rawUrl, name).replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, (_m, p) => `{{params.${draft.param(params, p, { type: 'string', required: true }, name)}}}`);
370
+ // `?a=1&b=2` in the raw URL is kept as written; the structured `query`
371
+ // array is what a caller edits in Postman, so it wins where present.
372
+ const query = typeof request.url === 'string' ? [] : (request.url?.query ?? []);
373
+ const enabled = query.filter((q) => q.key && !q.disabled);
374
+ if (enabled.length > 0) {
375
+ url = url.split('?')[0];
376
+ const pairs = enabled.map((q) => {
377
+ const key = q.key;
378
+ const param = draft.param(params, key, { type: 'string', ...(q.description ? { description: q.description } : {}) }, name);
379
+ return `${encodeURIComponent(key)}={{paramsEncoded.${param}}}`;
380
+ });
381
+ url = `${url}?${pairs.join('&')}`;
382
+ }
383
+ const headerPairs = {};
384
+ for (const h of request.header ?? []) {
385
+ if (h.key && !h.disabled)
386
+ headerPairs[h.key] = resolveVars(h.value ?? '', name);
387
+ }
388
+ // Collection-level auth applies to a request that declares none.
389
+ const auth = request.auth ?? collection.auth;
390
+ if (auth?.type && !Object.keys(headerPairs).some((k) => CREDENTIAL_HEADERS.has(k.toLowerCase()))) {
391
+ const secret = secretNameFor('authorization', options);
392
+ headerPairs.Authorization =
393
+ auth.type === 'basic' ? `Basic {{secrets.${secret}}}` : `Bearer {{secrets.${secret}}}`;
394
+ draft.secret(secret, name, `collection auth (${auth.type})`);
395
+ }
396
+ const headers = safeHeaders(headerPairs, name, draft, options, 'Postman');
397
+ let body;
398
+ const mode = request.body?.mode;
399
+ if (mode === 'raw' && request.body?.raw) {
400
+ body = resolveVars(request.body.raw, name);
401
+ }
402
+ else if (mode && mode !== 'raw' && mode !== 'none') {
403
+ draft.warnings.push({
404
+ kind: 'unsupported-body',
405
+ tool: name,
406
+ message: `Body mode "${mode}" is not supported; the tool was created without a body.`,
407
+ });
408
+ }
409
+ draft.tools.push({
410
+ name,
411
+ description: describe([item.description, request.description, trail.filter(Boolean).join(' / ')], name, method, rawUrl, draft),
412
+ method: (['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].includes(method)
413
+ ? method
414
+ : 'GET'),
415
+ url,
416
+ ...(Object.keys(headers).length > 0 && { headers }),
417
+ ...(Object.keys(params).length > 0 && { params }),
418
+ // DECLARED, because the placeholder alone does not reach the runtime.
419
+ ...(draft.secretsFor(name).length > 0 && { secrets: draft.secretsFor(name) }),
420
+ // GET carries no body: the API refuses one at authoring time.
421
+ ...(body !== undefined && method !== 'GET' && { body }),
422
+ });
423
+ }
424
+ };
425
+ walk(collection.item ?? [], []);
426
+ return draft.finish(options);
427
+ }
428
+ const OPENAPI_METHODS = ['get', 'post', 'put', 'delete', 'patch'];
429
+ function openApiType(t) {
430
+ if (t === 'integer' || t === 'number')
431
+ return 'number';
432
+ if (t === 'boolean')
433
+ return 'boolean';
434
+ return 'string';
435
+ }
436
+ /**
437
+ * Draft tools from an OpenAPI 3 or Swagger 2 document.
438
+ *
439
+ * WHAT THIS GIVES THAT POSTMAN DOES NOT: real parameter types and required
440
+ * flags, which is exactly the shape `HttpToolParam` wants, and an enum where the
441
+ * document declares one. FastAPI publishes this at `/openapi.json` without
442
+ * anybody writing it, so for a FastAPI shop this is usually the richer source.
443
+ *
444
+ * WHAT IT STILL DOES NOT GIVE: a description a model can choose on. `summary`
445
+ * is written for a developer reading a reference. Read the warnings.
446
+ */
447
+ export function fromOpenApi(doc, options = {}) {
448
+ const draft = new Draft();
449
+ // OpenAPI 3 `servers`, or Swagger 2's three separate fields.
450
+ const base = doc.servers?.[0]?.url ??
451
+ (doc.host ? `${doc.schemes?.[0] ?? 'https'}://${doc.host}${doc.basePath ?? ''}` : '');
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
+ }
459
+ const schemes = doc.components?.securitySchemes ?? doc.securityDefinitions ?? {};
460
+ const firstScheme = Object.values(schemes)[0];
461
+ for (const [path, operations] of Object.entries(doc.paths ?? {})) {
462
+ for (const method of OPENAPI_METHODS) {
463
+ const op = operations[method];
464
+ if (!op)
465
+ continue;
466
+ const candidateName = toolNameFrom(op.operationId ?? `${method}_${path}`, 'op');
467
+ const fullUrl = `${base}${path}`;
468
+ if (options.include && !options.include({ name: candidateName, method: method.toUpperCase(), url: fullUrl })) {
469
+ continue;
470
+ }
471
+ const name = draft.uniqueName(candidateName);
472
+ const params = {};
473
+ const headerPairs = {};
474
+ // A path template's `{order-id}` is a wire name like any other, so it
475
+ // gets the same normalisation as a query key. Declared or not: a brace
476
+ // in the path has to resolve, and an operation that never declared the
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;
497
+ }
498
+ // A cookie parameter cannot be expressed: it would have to be merged
499
+ // into a single Cookie header with every other one. Say so rather
500
+ // than declaring a param that goes nowhere.
501
+ if (p.in === 'cookie') {
502
+ draft.warnings.push({
503
+ kind: 'unsupported-body',
504
+ tool: name,
505
+ message: `Cookie parameter "${p.name}" is not supported; the tool was created without it.`,
506
+ });
507
+ continue;
508
+ }
509
+ const param = draft.param(params, p.name, def, name);
510
+ if (p.in === 'query') {
511
+ queryPairs.push(`${encodeURIComponent(p.name)}={{paramsEncoded.${param}}}`);
512
+ }
513
+ else if (p.in === 'header') {
514
+ // DECLARED AND NEVER SENT is the failure this closes. Every
515
+ // parameter used to land in `params` whatever its `in` was, but
516
+ // only query and path were wired to anything, so a required
517
+ // `X-Tenant-Id` made the model supply a value that was dropped on
518
+ // the floor and the request went out without the header.
519
+ headerPairs[p.name] = `{{params.${param}}}`;
520
+ }
521
+ }
522
+ if (queryPairs.length > 0)
523
+ url = `${url}?${queryPairs.join('&')}`;
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'})`);
534
+ }
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
+ }
567
+ }
568
+ return draft.finish(options);
569
+ }
570
+ /**
571
+ * Fetch an OpenAPI document and draft tools from it.
572
+ *
573
+ * THE FETCH GOES TO YOUR SERVER, NOT OURS. Pass `headers` if the document itself
574
+ * is behind auth. Nothing about the document is sent to AgentNava: the drafts
575
+ * are built in this process and you decide what to do with them.
576
+ */
577
+ export async function fromOpenApiUrl(url, options = {}) {
578
+ const f = options.fetch ?? fetch;
579
+ const res = await f(url, { headers: options.headers ?? {} });
580
+ if (!res.ok) {
581
+ throw new Error(`could not read the OpenAPI document at ${url}: ${res.status}`);
582
+ }
583
+ return fromOpenApi((await res.json()), options);
584
+ }
585
+ // ------------------------------------------------------------------- curl
586
+ /**
587
+ * Draft one tool from a curl command.
588
+ *
589
+ * THE LOWEST BAR ON PURPOSE. A team with no collection and no document still has
590
+ * a curl command that works, usually pasted in a chat thread, and that is enough
591
+ * to start. Handles the flags people actually use: `-X`, `-H`, `-d`/`--data`,
592
+ * `--data-raw`, `--url`, and a bare URL.
593
+ *
594
+ * Give it a `name` and a `description`: a curl command carries neither, and the
595
+ * description is the field that decides whether the agent picks this tool.
596
+ */
597
+ export function fromCurl(command, options = {}) {
598
+ const draft = new Draft();
599
+ // Join line continuations, then split on whitespace outside quotes.
600
+ const flat = command.replace(/\\\r?\n/g, ' ').trim();
601
+ const tokens = flat.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
602
+ const unquote = (s) => (s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))
603
+ ? s.slice(1, -1)
604
+ : s;
605
+ let method = '';
606
+ let url = '';
607
+ let body;
608
+ const headerPairs = {};
609
+ for (let i = 0; i < tokens.length; i++) {
610
+ const t = tokens[i];
611
+ if (t === 'curl')
612
+ continue;
613
+ if (t === '-X' || t === '--request') {
614
+ method = unquote(tokens[++i] ?? 'GET').toUpperCase();
615
+ }
616
+ else if (t === '-H' || t === '--header') {
617
+ const raw = unquote(tokens[++i] ?? '');
618
+ const at = raw.indexOf(':');
619
+ if (at > 0)
620
+ headerPairs[raw.slice(0, at).trim()] = raw.slice(at + 1).trim();
621
+ }
622
+ else if (t === '-d' || t === '--data' || t === '--data-raw' || t === '--data-binary') {
623
+ body = unquote(tokens[++i] ?? '');
624
+ }
625
+ else if (t === '-u' || t === '--user') {
626
+ // `-u user:pass` IS a credential, and it used to vanish without a trace:
627
+ // `-u` matched no branch, and `user:pass` matched neither `^-` nor a
628
+ // url, so both tokens were skipped in silence. Routed through the
629
+ // Authorization header so safeHeaders strips it like any other, which
630
+ // also produces the credential-stripped warning.
631
+ headerPairs.Authorization = `Basic ${unquote(tokens[++i] ?? '')}`;
632
+ }
633
+ else if (t === '-b' || t === '--cookie') {
634
+ headerPairs.Cookie = unquote(tokens[++i] ?? '');
635
+ }
636
+ else if (t === '--url') {
637
+ url = unquote(tokens[++i] ?? '');
638
+ }
639
+ else if (!t.startsWith('-') && /^https?:\/\//.test(unquote(t))) {
640
+ url = unquote(t);
641
+ }
642
+ }
643
+ if (!url)
644
+ throw new Error('no URL found in that curl command');
645
+ // curl's own default: a body implies POST unless -X said otherwise.
646
+ if (!method)
647
+ method = body !== undefined ? 'POST' : 'GET';
648
+ const name = draft.uniqueName(toolNameFrom(options.name ?? `${method} ${url}`));
649
+ const headers = safeHeaders(headerPairs, name, draft, options, 'curl');
650
+ draft.tools.push({
651
+ name,
652
+ description: describe([options.description], name, method, url, draft),
653
+ method: (['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].includes(method)
654
+ ? method
655
+ : 'GET'),
656
+ url,
657
+ ...(Object.keys(headers).length > 0 && { headers }),
658
+ ...(draft.secretsFor(name).length > 0 && { secrets: draft.secretsFor(name) }),
659
+ ...(body !== undefined && method !== 'GET' && { body }),
660
+ });
661
+ return draft.finish(options);
662
+ }
663
+ export function importTools(source, options = {}) {
664
+ if ('openapi' in source)
665
+ return fromOpenApi(source.openapi, options);
666
+ if ('postman' in source)
667
+ return fromPostman(source.postman, options);
668
+ if ('curl' in source) {
669
+ return fromCurl(source.curl, {
670
+ ...options,
671
+ ...(source.name !== undefined && { name: source.name }),
672
+ ...(source.description !== undefined && { description: source.description }),
673
+ });
674
+ }
675
+ if ('openapiUrl' in source) {
676
+ return fromOpenApiUrl(source.openapiUrl, {
677
+ ...options,
678
+ ...(source.headers !== undefined && { headers: source.headers }),
679
+ ...(source.fetch !== undefined && { fetch: source.fetch }),
680
+ });
681
+ }
682
+ // Named, not "invalid source": a caller who passed the wrong key wants to
683
+ // know which keys exist.
684
+ throw new Error('importTools needs one of: { openapi }, { openapiUrl }, { postman }, { curl }');
685
+ }
686
+ //# sourceMappingURL=import-tools.js.map