toolnexus 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js ADDED
@@ -0,0 +1,447 @@
1
+ const RETRYABLE = new Set([429, 500, 502, 503, 504]);
2
+ function delay(ms, signal) {
3
+ return new Promise((resolve, reject) => {
4
+ const t = setTimeout(resolve, ms);
5
+ signal.addEventListener("abort", () => { clearTimeout(t); reject(signal.reason ?? new Error("aborted")); }, { once: true });
6
+ });
7
+ }
8
+ function addUsage(acc, raw, style) {
9
+ if (!raw)
10
+ return;
11
+ if (style === "anthropic") {
12
+ acc.promptTokens += raw.input_tokens ?? 0;
13
+ acc.completionTokens += raw.output_tokens ?? 0;
14
+ acc.totalTokens += (raw.input_tokens ?? 0) + (raw.output_tokens ?? 0);
15
+ }
16
+ else {
17
+ acc.promptTokens += raw.prompt_tokens ?? 0;
18
+ acc.completionTokens += raw.completion_tokens ?? 0;
19
+ acc.totalTokens += raw.total_tokens ?? (raw.prompt_tokens ?? 0) + (raw.completion_tokens ?? 0);
20
+ }
21
+ }
22
+ function resolveKey(opts) {
23
+ if (opts.apiKey)
24
+ return opts.apiKey;
25
+ const env = process.env;
26
+ const key = env.OPENROUTER_API_KEY ||
27
+ (opts.style === "anthropic" ? env.ANTHROPIC_API_KEY : env.OPENAI_API_KEY) ||
28
+ env.OPENAI_API_KEY ||
29
+ env.ANTHROPIC_API_KEY;
30
+ if (!key)
31
+ throw new Error("No API key: set apiKey or OPENROUTER_API_KEY / OPENAI_API_KEY / ANTHROPIC_API_KEY");
32
+ return key;
33
+ }
34
+ export class Client {
35
+ opts;
36
+ constructor(opts) {
37
+ this.opts = opts;
38
+ }
39
+ async run(prompt, ctx) {
40
+ return this.opts.style === "anthropic"
41
+ ? this.runAnthropic(prompt, ctx.toolkit, ctx.signal, ctx.history)
42
+ : this.runOpenAI(prompt, ctx.toolkit, ctx.signal, ctx.history);
43
+ }
44
+ /** A stateful multi-turn conversation that retains history across sends. */
45
+ conversation(ctx) {
46
+ return new Conversation(this, ctx.toolkit, ctx.signal);
47
+ }
48
+ /** Streaming variant: async-iterate live events (text deltas, tool calls/results, usage, done). */
49
+ stream(prompt, ctx) {
50
+ return this.opts.style === "anthropic"
51
+ ? this.streamAnthropic(prompt, ctx.toolkit, ctx.signal)
52
+ : this.streamOpenAI(prompt, ctx.toolkit, ctx.signal);
53
+ }
54
+ system(toolkit) {
55
+ return [this.opts.systemPrompt ?? "", toolkit.skillsPrompt()].filter(Boolean).join("\n\n");
56
+ }
57
+ /** Build a run-scoped abort signal from the optional run-level timeout + an external signal. */
58
+ makeSignal(external) {
59
+ const ctrl = new AbortController();
60
+ if (this.opts.timeoutMs) {
61
+ const t = setTimeout(() => ctrl.abort(new Error(`run timeout after ${this.opts.timeoutMs}ms`)), this.opts.timeoutMs);
62
+ t.unref?.();
63
+ }
64
+ if (external) {
65
+ if (external.aborted)
66
+ ctrl.abort(external.reason);
67
+ else
68
+ external.addEventListener("abort", () => ctrl.abort(external.reason), { once: true });
69
+ }
70
+ return ctrl.signal;
71
+ }
72
+ /** fetch with retry + exponential backoff on 429/5xx/network, honoring Retry-After; aborts via signal. */
73
+ async llmFetch(url, init, signal) {
74
+ const retries = this.opts.retries ?? 2;
75
+ const base = this.opts.retryBaseMs ?? 500;
76
+ let lastErr;
77
+ for (let attempt = 0; attempt <= retries; attempt++) {
78
+ try {
79
+ const res = await fetch(url, { ...init, signal });
80
+ if (res.ok || !RETRYABLE.has(res.status) || attempt === retries)
81
+ return res;
82
+ const ra = Number(res.headers.get("retry-after"));
83
+ await delay(ra ? ra * 1000 : base * 2 ** attempt + Math.random() * 100, signal);
84
+ }
85
+ catch (e) {
86
+ if (signal.aborted)
87
+ throw e; // abort/timeout: don't retry
88
+ lastErr = e;
89
+ if (attempt === retries)
90
+ throw e;
91
+ await delay(base * 2 ** attempt + Math.random() * 100, signal);
92
+ }
93
+ }
94
+ throw lastErr;
95
+ }
96
+ /** Run one tool through beforeTool/afterTool hooks (mutate args, short-circuit, transform result). */
97
+ async runTool(toolkit, name, args, id, turn) {
98
+ const h = this.opts.hooks;
99
+ let a = args;
100
+ if (h?.beforeTool) {
101
+ const ov = await h.beforeTool({ name, args: a, id, turn });
102
+ if (ov?.result)
103
+ return { args: a, result: ov.result }; // short-circuit (deny/cache/dry-run)
104
+ if (ov?.args)
105
+ a = ov.args;
106
+ }
107
+ let result = await toolkit.execute(name, a);
108
+ if (h?.afterTool) {
109
+ const ov = await h.afterTool({ name, args: a, result, id, turn });
110
+ if (ov?.result)
111
+ result = ov.result;
112
+ }
113
+ return { args: a, result };
114
+ }
115
+ // ---- OpenAI-style: POST {baseUrl}/chat/completions ----
116
+ async runOpenAI(prompt, toolkit, external, history) {
117
+ const key = resolveKey(this.opts);
118
+ const signal = this.makeSignal(external);
119
+ let messages = history && history.length ? [...history] : [];
120
+ if (!messages.length) {
121
+ const system = this.system(toolkit);
122
+ if (system)
123
+ messages.push({ role: "system", content: system });
124
+ }
125
+ messages.push({ role: "user", content: prompt });
126
+ let tools = toolkit.toOpenAI();
127
+ const toolCalls = [];
128
+ const usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
129
+ let turns = 0;
130
+ for (let turn = 0; turn < (this.opts.maxTurns ?? 10); turn++) {
131
+ turns++;
132
+ if (this.opts.hooks?.beforeLLM) {
133
+ const ov = await this.opts.hooks.beforeLLM({ messages, tools, model: this.opts.model, turn });
134
+ if (ov?.messages)
135
+ messages = ov.messages;
136
+ if (ov?.tools)
137
+ tools = ov.tools;
138
+ }
139
+ const res = await this.llmFetch(`${this.opts.baseUrl.replace(/\/$/, "")}/chat/completions`, {
140
+ method: "POST",
141
+ headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json", ...this.opts.headers },
142
+ body: JSON.stringify({ model: this.opts.model, messages, tools, tool_choice: "auto" }),
143
+ }, signal);
144
+ if (!res.ok)
145
+ throw new Error(`LLM ${res.status}: ${await res.text()}`);
146
+ const data = await res.json();
147
+ addUsage(usage, data.usage, "openai");
148
+ if (this.opts.hooks?.afterLLM)
149
+ await this.opts.hooks.afterLLM({ response: data, model: this.opts.model, turn });
150
+ const msg = data.choices[0].message;
151
+ messages.push(msg);
152
+ const calls = msg.tool_calls ?? [];
153
+ if (calls.length === 0)
154
+ return this.result(msg.content ?? "", messages, toolCalls, turns, usage);
155
+ // execute all tool calls in this turn concurrently (true parallel tool calling)
156
+ const results = await Promise.all(calls.map(async (call) => {
157
+ const { args, result } = await this.runTool(toolkit, call.function.name, safeJson(call.function.arguments), call.id, turn);
158
+ toolCalls.push({ name: call.function.name, args, output: result.output, isError: result.isError, metadata: result.metadata });
159
+ return { role: "tool", tool_call_id: call.id, content: result.output };
160
+ }));
161
+ messages.push(...results);
162
+ }
163
+ return this.result(lastText(messages), messages, toolCalls, turns, usage);
164
+ }
165
+ result(text, messages, toolCalls, turns, usage) {
166
+ return { text, messages, toolCalls, toolCallCount: toolCalls.length, turns, usage, model: this.opts.model };
167
+ }
168
+ // ---- Anthropic-style: POST {baseUrl}/messages ----
169
+ async runAnthropic(prompt, toolkit, external, history) {
170
+ const key = resolveKey(this.opts);
171
+ const signal = this.makeSignal(external);
172
+ const base = this.opts.baseUrl.replace(/\/$/, "");
173
+ const endpoint = base.endsWith("/v1") ? `${base}/messages` : `${base}/v1/messages`;
174
+ const system = this.system(toolkit);
175
+ let messages = history && history.length ? [...history, { role: "user", content: prompt }] : [{ role: "user", content: prompt }];
176
+ let tools = toolkit.toAnthropic();
177
+ const toolCalls = [];
178
+ const usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
179
+ let turns = 0;
180
+ for (let turn = 0; turn < (this.opts.maxTurns ?? 10); turn++) {
181
+ turns++;
182
+ if (this.opts.hooks?.beforeLLM) {
183
+ const ov = await this.opts.hooks.beforeLLM({ messages, tools, model: this.opts.model, turn });
184
+ if (ov?.messages)
185
+ messages = ov.messages;
186
+ if (ov?.tools)
187
+ tools = ov.tools;
188
+ }
189
+ const res = await this.llmFetch(endpoint, {
190
+ method: "POST",
191
+ headers: {
192
+ "x-api-key": key,
193
+ "anthropic-version": "2023-06-01",
194
+ "Content-Type": "application/json",
195
+ ...this.opts.headers,
196
+ },
197
+ body: JSON.stringify({ model: this.opts.model, max_tokens: 4096, system, messages, tools }),
198
+ }, signal);
199
+ if (!res.ok)
200
+ throw new Error(`LLM ${res.status}: ${await res.text()}`);
201
+ const data = await res.json();
202
+ addUsage(usage, data.usage, "anthropic");
203
+ if (this.opts.hooks?.afterLLM)
204
+ await this.opts.hooks.afterLLM({ response: data, model: this.opts.model, turn });
205
+ messages.push({ role: "assistant", content: data.content });
206
+ const uses = (data.content ?? []).filter((b) => b.type === "tool_use");
207
+ if (uses.length === 0) {
208
+ const text = (data.content ?? []).filter((b) => b.type === "text").map((b) => b.text).join("");
209
+ return this.result(text, messages, toolCalls, turns, usage);
210
+ }
211
+ // execute all tool_use blocks in this turn concurrently (true parallel tool calling)
212
+ const results = await Promise.all(uses.map(async (use) => {
213
+ const { args, result } = await this.runTool(toolkit, use.name, use.input ?? {}, use.id, turn);
214
+ toolCalls.push({ name: use.name, args, output: result.output, isError: result.isError, metadata: result.metadata });
215
+ return { type: "tool_result", tool_use_id: use.id, content: result.output, is_error: result.isError };
216
+ }));
217
+ messages.push({ role: "user", content: results });
218
+ }
219
+ return this.result("", messages, toolCalls, turns, usage);
220
+ }
221
+ // ---- Streaming: OpenAI-style ----
222
+ async *streamOpenAI(prompt, toolkit, external) {
223
+ const key = resolveKey(this.opts);
224
+ const signal = this.makeSignal(external);
225
+ let messages = [];
226
+ const system = this.system(toolkit);
227
+ if (system)
228
+ messages.push({ role: "system", content: system });
229
+ messages.push({ role: "user", content: prompt });
230
+ let tools = toolkit.toOpenAI();
231
+ const toolCalls = [];
232
+ const usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
233
+ let turns = 0;
234
+ for (let turn = 0; turn < (this.opts.maxTurns ?? 10); turn++) {
235
+ turns++;
236
+ if (this.opts.hooks?.beforeLLM) {
237
+ const ov = await this.opts.hooks.beforeLLM({ messages, tools, model: this.opts.model, turn });
238
+ if (ov?.messages)
239
+ messages = ov.messages;
240
+ if (ov?.tools)
241
+ tools = ov.tools;
242
+ }
243
+ const res = await this.llmFetch(`${this.opts.baseUrl.replace(/\/$/, "")}/chat/completions`, {
244
+ method: "POST",
245
+ headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json", ...this.opts.headers },
246
+ body: JSON.stringify({ model: this.opts.model, messages, tools, tool_choice: "auto", stream: true, stream_options: { include_usage: true } }),
247
+ }, signal);
248
+ if (!res.ok || !res.body)
249
+ throw new Error(`LLM ${res.status}: ${await res.text()}`);
250
+ let content = "";
251
+ const acc = new Map();
252
+ for await (const line of sseLines(res.body)) {
253
+ if (!line.startsWith("data:"))
254
+ continue;
255
+ const payload = line.slice(5).trim();
256
+ if (payload === "[DONE]")
257
+ break;
258
+ const j = safeParse(payload);
259
+ if (j?.usage)
260
+ addUsage(usage, j.usage, "openai");
261
+ const choice = j?.choices?.[0];
262
+ const d = choice?.delta;
263
+ if (d?.content) {
264
+ content += d.content;
265
+ yield { type: "text", delta: d.content };
266
+ }
267
+ for (const tc of d?.tool_calls ?? []) {
268
+ const slot = acc.get(tc.index) ?? { id: "", name: "", args: "" };
269
+ if (tc.id)
270
+ slot.id = tc.id;
271
+ if (tc.function?.name)
272
+ slot.name += tc.function.name;
273
+ if (tc.function?.arguments)
274
+ slot.args += tc.function.arguments;
275
+ acc.set(tc.index, slot);
276
+ }
277
+ }
278
+ if (this.opts.hooks?.afterLLM)
279
+ await this.opts.hooks.afterLLM({ response: { streamed: true, usage }, model: this.opts.model, turn });
280
+ const calls = [...acc.values()];
281
+ if (calls.length === 0) {
282
+ messages.push({ role: "assistant", content });
283
+ yield { type: "usage", usage };
284
+ yield { type: "done", result: this.result(content, messages, toolCalls, turns, usage) };
285
+ return;
286
+ }
287
+ messages.push({ role: "assistant", content: content || null, tool_calls: calls.map((c) => ({ id: c.id, type: "function", function: { name: c.name, arguments: c.args } })) });
288
+ for (const c of calls)
289
+ yield { type: "tool_call", id: c.id, name: c.name, args: safeJson(c.args) };
290
+ const settled = await Promise.all(calls.map((c) => this.runTool(toolkit, c.name, safeJson(c.args), c.id, turn)));
291
+ for (let i = 0; i < calls.length; i++) {
292
+ const c = calls[i], { args, result } = settled[i];
293
+ toolCalls.push({ name: c.name, args, output: result.output, isError: result.isError, metadata: result.metadata });
294
+ messages.push({ role: "tool", tool_call_id: c.id, content: result.output });
295
+ yield { type: "tool_result", id: c.id, name: c.name, output: result.output, isError: result.isError };
296
+ }
297
+ }
298
+ yield { type: "done", result: this.result(lastText(messages), messages, toolCalls, turns, usage) };
299
+ }
300
+ // ---- Streaming: Anthropic-style ----
301
+ async *streamAnthropic(prompt, toolkit, external) {
302
+ const key = resolveKey(this.opts);
303
+ const signal = this.makeSignal(external);
304
+ const base = this.opts.baseUrl.replace(/\/$/, "");
305
+ const endpoint = base.endsWith("/v1") ? `${base}/messages` : `${base}/v1/messages`;
306
+ const system = this.system(toolkit);
307
+ let messages = [{ role: "user", content: prompt }];
308
+ let tools = toolkit.toAnthropic();
309
+ const toolCalls = [];
310
+ const usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
311
+ let turns = 0;
312
+ for (let turn = 0; turn < (this.opts.maxTurns ?? 10); turn++) {
313
+ turns++;
314
+ if (this.opts.hooks?.beforeLLM) {
315
+ const ov = await this.opts.hooks.beforeLLM({ messages, tools, model: this.opts.model, turn });
316
+ if (ov?.messages)
317
+ messages = ov.messages;
318
+ if (ov?.tools)
319
+ tools = ov.tools;
320
+ }
321
+ const res = await this.llmFetch(endpoint, {
322
+ method: "POST",
323
+ headers: { "x-api-key": key, "anthropic-version": "2023-06-01", "Content-Type": "application/json", ...this.opts.headers },
324
+ body: JSON.stringify({ model: this.opts.model, max_tokens: 4096, system, messages, tools, stream: true }),
325
+ }, signal);
326
+ if (!res.ok || !res.body)
327
+ throw new Error(`LLM ${res.status}: ${await res.text()}`);
328
+ const blocks = new Map();
329
+ let stopReason = "";
330
+ for await (const line of sseLines(res.body)) {
331
+ if (!line.startsWith("data:"))
332
+ continue;
333
+ const j = safeParse(line.slice(5).trim());
334
+ if (!j)
335
+ continue;
336
+ if (j.type === "message_start")
337
+ addUsage(usage, j.message?.usage, "anthropic");
338
+ else if (j.type === "content_block_start")
339
+ blocks.set(j.index, { type: j.content_block.type, id: j.content_block.id, name: j.content_block.name, text: "", json: "" });
340
+ else if (j.type === "content_block_delta") {
341
+ const b = blocks.get(j.index);
342
+ if (j.delta.type === "text_delta") {
343
+ b.text += j.delta.text;
344
+ yield { type: "text", delta: j.delta.text };
345
+ }
346
+ else if (j.delta.type === "input_json_delta")
347
+ b.json += j.delta.partial_json;
348
+ }
349
+ else if (j.type === "message_delta") {
350
+ stopReason = j.delta?.stop_reason ?? stopReason;
351
+ addUsage(usage, j.usage, "anthropic");
352
+ }
353
+ }
354
+ if (this.opts.hooks?.afterLLM)
355
+ await this.opts.hooks.afterLLM({ response: { streamed: true, usage }, model: this.opts.model, turn });
356
+ const content = [...blocks.values()].map((b) => b.type === "tool_use" ? { type: "tool_use", id: b.id, name: b.name, input: safeJson(b.json || "{}") } : { type: "text", text: b.text });
357
+ messages.push({ role: "assistant", content });
358
+ const uses = content.filter((b) => b.type === "tool_use");
359
+ if (stopReason !== "tool_use" || uses.length === 0) {
360
+ const text = content.filter((b) => b.type === "text").map((b) => b.text).join("");
361
+ yield { type: "usage", usage };
362
+ yield { type: "done", result: this.result(text, messages, toolCalls, turns, usage) };
363
+ return;
364
+ }
365
+ for (const u of uses)
366
+ yield { type: "tool_call", id: u.id, name: u.name, args: u.input };
367
+ const settled = await Promise.all(uses.map((u) => this.runTool(toolkit, u.name, u.input ?? {}, u.id, turn)));
368
+ const results = [];
369
+ for (let i = 0; i < uses.length; i++) {
370
+ const u = uses[i], { args, result } = settled[i];
371
+ toolCalls.push({ name: u.name, args, output: result.output, isError: result.isError, metadata: result.metadata });
372
+ results.push({ type: "tool_result", tool_use_id: u.id, content: result.output, is_error: result.isError });
373
+ yield { type: "tool_result", id: u.id, name: u.name, output: result.output, isError: result.isError };
374
+ }
375
+ messages.push({ role: "user", content: results });
376
+ }
377
+ yield { type: "done", result: this.result("", messages, toolCalls, turns, usage) };
378
+ }
379
+ }
380
+ export function createClient(opts) {
381
+ return new Client(opts);
382
+ }
383
+ /** Stateful multi-turn conversation: each send() continues the same transcript (memory). */
384
+ export class Conversation {
385
+ client;
386
+ toolkit;
387
+ signal;
388
+ /** Full running transcript (system + user + assistant + tool messages). */
389
+ messages = [];
390
+ constructor(client, toolkit, signal) {
391
+ this.client = client;
392
+ this.toolkit = toolkit;
393
+ this.signal = signal;
394
+ }
395
+ /** Send the next user turn; prior history is retained automatically. */
396
+ async send(prompt) {
397
+ const result = await this.client.run(prompt, { toolkit: this.toolkit, signal: this.signal, history: this.messages });
398
+ this.messages = result.messages;
399
+ return result;
400
+ }
401
+ /** Reset the conversation memory. */
402
+ reset() {
403
+ this.messages = [];
404
+ }
405
+ }
406
+ /** Read an SSE stream line-by-line from a fetch body (web ReadableStream). */
407
+ async function* sseLines(body) {
408
+ const reader = body.getReader();
409
+ const decoder = new TextDecoder();
410
+ let buf = "";
411
+ while (true) {
412
+ const { done, value } = await reader.read();
413
+ if (done)
414
+ break;
415
+ buf += decoder.decode(value, { stream: true });
416
+ let idx;
417
+ while ((idx = buf.indexOf("\n")) >= 0) {
418
+ yield buf.slice(0, idx).replace(/\r$/, "");
419
+ buf = buf.slice(idx + 1);
420
+ }
421
+ }
422
+ if (buf)
423
+ yield buf;
424
+ }
425
+ function safeParse(s) {
426
+ try {
427
+ return JSON.parse(s);
428
+ }
429
+ catch {
430
+ return undefined;
431
+ }
432
+ }
433
+ function safeJson(s) {
434
+ try {
435
+ return JSON.parse(s || "{}");
436
+ }
437
+ catch {
438
+ return {};
439
+ }
440
+ }
441
+ function lastText(messages) {
442
+ for (let i = messages.length - 1; i >= 0; i--) {
443
+ if (messages[i].role === "assistant" && typeof messages[i].content === "string")
444
+ return messages[i].content;
445
+ }
446
+ return "";
447
+ }
package/dist/http.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * HTTP / REST tools — declare a remote endpoint as a uniform Tool.
3
+ * See ../../SPEC.md §7.
4
+ */
5
+ import type { JSONSchema, Tool } from "./types.js";
6
+ export interface HttpToolOptions {
7
+ name: string;
8
+ description: string;
9
+ method: string;
10
+ url: string;
11
+ headers?: Record<string, string>;
12
+ /** Arg names sent as querystring instead of in the body. */
13
+ query?: string[];
14
+ /** Body encoding for non-GET requests. */
15
+ body?: "json" | "form" | "raw";
16
+ inputSchema?: JSONSchema;
17
+ timeout?: number;
18
+ resultMode?: "text" | "json" | "status+text";
19
+ }
20
+ /** Wrap an HTTP endpoint as a Tool. */
21
+ export declare function httpTool(opts: HttpToolOptions): Tool;
package/dist/http.js ADDED
@@ -0,0 +1,90 @@
1
+ const DEFAULT_TIMEOUT = 30_000;
2
+ const EMPTY_SCHEMA = { type: "object", properties: {}, additionalProperties: false };
3
+ /** Expand ${ENV_VAR} in header values from process.env (never logged). */
4
+ function expandHeaders(headers = {}) {
5
+ const out = {};
6
+ for (const [k, v] of Object.entries(headers)) {
7
+ out[k] = v.replace(/\$\{([A-Z0-9_]+)\}/gi, (_, name) => process.env[name] ?? "");
8
+ }
9
+ return out;
10
+ }
11
+ /** Wrap an HTTP endpoint as a Tool. */
12
+ export function httpTool(opts) {
13
+ const method = opts.method.toUpperCase();
14
+ const querySet = new Set(opts.query ?? []);
15
+ return {
16
+ name: opts.name,
17
+ description: opts.description,
18
+ inputSchema: opts.inputSchema ?? EMPTY_SCHEMA,
19
+ source: "http",
20
+ async execute(args, ctx) {
21
+ const a = { ...(args ?? {}) };
22
+ // 1. substitute {placeholders} in the URL from args (consumed afterwards)
23
+ let url = opts.url.replace(/\{(\w+)\}/g, (_, key) => {
24
+ const val = a[key];
25
+ delete a[key];
26
+ return encodeURIComponent(String(val ?? ""));
27
+ });
28
+ // 2. querystring args
29
+ const params = new URLSearchParams();
30
+ for (const key of Object.keys(a)) {
31
+ if (querySet.has(key) || method === "GET") {
32
+ params.append(key, String(a[key]));
33
+ delete a[key];
34
+ }
35
+ }
36
+ const qs = params.toString();
37
+ if (qs)
38
+ url += (url.includes("?") ? "&" : "?") + qs;
39
+ // 3. body
40
+ const headers = expandHeaders(opts.headers);
41
+ let bodyInit;
42
+ if (method !== "GET" && method !== "HEAD" && Object.keys(a).length > 0) {
43
+ const mode = opts.body ?? "json";
44
+ if (mode === "json") {
45
+ headers["Content-Type"] ??= "application/json";
46
+ bodyInit = JSON.stringify(a);
47
+ }
48
+ else if (mode === "form") {
49
+ headers["Content-Type"] ??= "application/x-www-form-urlencoded";
50
+ bodyInit = new URLSearchParams(a).toString();
51
+ }
52
+ else {
53
+ bodyInit = String(a.body ?? "");
54
+ }
55
+ }
56
+ const timeout = ctx?.timeout ?? opts.timeout ?? DEFAULT_TIMEOUT;
57
+ const controller = new AbortController();
58
+ const timer = setTimeout(() => controller.abort(), timeout);
59
+ if (ctx?.signal)
60
+ ctx.signal.addEventListener("abort", () => controller.abort(), { once: true });
61
+ try {
62
+ const res = await fetch(url, { method, headers, body: bodyInit, signal: controller.signal });
63
+ const text = await res.text();
64
+ if (!res.ok) {
65
+ return { output: `HTTP ${res.status}: ${text}`, isError: true, metadata: { status: res.status } };
66
+ }
67
+ const output = opts.resultMode === "status+text"
68
+ ? `${res.status}\n${text}`
69
+ : opts.resultMode === "json"
70
+ ? JSON.stringify(safeParse(text))
71
+ : text;
72
+ return { output, isError: false, metadata: { status: res.status } };
73
+ }
74
+ catch (e) {
75
+ return { output: e instanceof Error ? e.message : String(e), isError: true };
76
+ }
77
+ finally {
78
+ clearTimeout(timer);
79
+ }
80
+ },
81
+ };
82
+ }
83
+ function safeParse(text) {
84
+ try {
85
+ return JSON.parse(text);
86
+ }
87
+ catch {
88
+ return text;
89
+ }
90
+ }
@@ -0,0 +1,8 @@
1
+ export * from "./types.js";
2
+ export * from "./mcp.js";
3
+ export * from "./skill.js";
4
+ export * from "./native.js";
5
+ export * from "./http.js";
6
+ export * from "./adapters.js";
7
+ export * from "./toolkit.js";
8
+ export * from "./client.js";
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export * from "./types.js";
2
+ export * from "./mcp.js";
3
+ export * from "./skill.js";
4
+ export * from "./native.js";
5
+ export * from "./http.js";
6
+ export * from "./adapters.js";
7
+ export * from "./toolkit.js";
8
+ export * from "./client.js";
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ import { type Tool, type McpStatus } from "./types.js";
2
+ export interface LocalServer {
3
+ type?: "local";
4
+ command: string[];
5
+ environment?: Record<string, string>;
6
+ env?: Record<string, string>;
7
+ cwd?: string;
8
+ enabled?: boolean;
9
+ disabled?: boolean;
10
+ timeout?: number;
11
+ }
12
+ export interface RemoteServer {
13
+ type?: "remote";
14
+ url: string;
15
+ headers?: Record<string, string>;
16
+ enabled?: boolean;
17
+ disabled?: boolean;
18
+ timeout?: number;
19
+ }
20
+ export type ServerConfig = LocalServer | RemoteServer;
21
+ export type McpConfig = Record<string, ServerConfig>;
22
+ /** Accept a path, a raw object, or an object wrapped under mcpServers/servers/mcp. */
23
+ export declare function parseMcpConfig(input: string | object): McpConfig;
24
+ /**
25
+ * Expand ${ENV_VAR} in header values from process.env so tokens live in the
26
+ * environment, not the committed config. Values are never logged.
27
+ */
28
+ export declare function expandEnvHeaders(headers?: Record<string, string>): Record<string, string> | undefined;
29
+ export interface McpSource {
30
+ tools: Tool[];
31
+ status: Record<string, McpStatus>;
32
+ close(): Promise<void>;
33
+ }
34
+ /** Connect to every enabled server, list + convert tools. Failures are isolated. */
35
+ export declare function loadMcp(input: string | object): Promise<McpSource>;