@ouraihub/llm 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/cjs/index.cjs +995 -0
- package/dist/esm/index.mjs +972 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types/anthropic-provider.d.ts +27 -0
- package/dist/types/anthropic-provider.d.ts.map +1 -0
- package/dist/types/base-provider.d.ts +72 -0
- package/dist/types/base-provider.d.ts.map +1 -0
- package/dist/types/errors.d.ts +20 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/index.d.ts +52 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/interfaces.d.ts +140 -0
- package/dist/types/interfaces.d.ts.map +1 -0
- package/dist/types/logger.d.ts +25 -0
- package/dist/types/logger.d.ts.map +1 -0
- package/dist/types/mcp-client.d.ts +81 -0
- package/dist/types/mcp-client.d.ts.map +1 -0
- package/dist/types/openai-provider.d.ts +29 -0
- package/dist/types/openai-provider.d.ts.map +1 -0
- package/dist/types/skill-loader.d.ts +82 -0
- package/dist/types/skill-loader.d.ts.map +1 -0
- package/dist/types/tool-executor.d.ts +41 -0
- package/dist/types/tool-executor.d.ts.map +1 -0
- package/package.json +54 -0
|
@@ -0,0 +1,972 @@
|
|
|
1
|
+
// src/logger.ts
|
|
2
|
+
var noopLogger = {
|
|
3
|
+
info: () => {
|
|
4
|
+
},
|
|
5
|
+
warn: () => {
|
|
6
|
+
},
|
|
7
|
+
error: () => {
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var consoleLogger = {
|
|
11
|
+
info: (msg, data) => console.log(JSON.stringify({ level: "info", msg, ...data, ts: (/* @__PURE__ */ new Date()).toISOString() })),
|
|
12
|
+
warn: (msg, data) => console.warn(JSON.stringify({ level: "warn", msg, ...data, ts: (/* @__PURE__ */ new Date()).toISOString() })),
|
|
13
|
+
error: (msg, data) => console.error(JSON.stringify({ level: "error", msg, ...data, ts: (/* @__PURE__ */ new Date()).toISOString() }))
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// src/errors.ts
|
|
17
|
+
var LLMError = class extends Error {
|
|
18
|
+
constructor(message, vendor, statusCode, durationMs) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "LLMError";
|
|
21
|
+
this.vendor = vendor;
|
|
22
|
+
this.statusCode = statusCode;
|
|
23
|
+
this.durationMs = durationMs;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var LLMTimeoutError = class extends LLMError {
|
|
27
|
+
constructor(vendor, timeoutMs) {
|
|
28
|
+
super(`${vendor} request timed out after ${timeoutMs}ms`, vendor);
|
|
29
|
+
this.name = "LLMTimeoutError";
|
|
30
|
+
this.timeoutMs = timeoutMs;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var LLMSchemaError = class extends LLMError {
|
|
34
|
+
constructor(vendor, details) {
|
|
35
|
+
super(`${vendor} output failed schema validation: ${details}`, vendor);
|
|
36
|
+
this.name = "LLMSchemaError";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/base-provider.ts
|
|
41
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
42
|
+
var DEFAULT_MAX_RETRIES = 2;
|
|
43
|
+
var RETRY_BASE_DELAY_MS = 1e3;
|
|
44
|
+
var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
45
|
+
var BaseLLMProvider = class {
|
|
46
|
+
constructor(config) {
|
|
47
|
+
this.apiKey = config.apiKey;
|
|
48
|
+
this.baseUrl = (config.baseUrl ?? this.getDefaultBaseUrl()).replace(/\/+$/, "");
|
|
49
|
+
this.model = config.model ?? this.getDefaultModel();
|
|
50
|
+
this.vendor = config.vendor ?? this.getVendorName();
|
|
51
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
52
|
+
this.logger = config.logger ?? noopLogger;
|
|
53
|
+
this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
54
|
+
}
|
|
55
|
+
/** Structured completion with Zod schema validation */
|
|
56
|
+
async complete(prompt, schema, options) {
|
|
57
|
+
const result = await this.raw(prompt, { ...options, jsonMode: true });
|
|
58
|
+
return this.parseAndValidate(result.content, schema);
|
|
59
|
+
}
|
|
60
|
+
/** Raw completion without schema enforcement */
|
|
61
|
+
async raw(prompt, options) {
|
|
62
|
+
const { url, init } = this.buildRequest(prompt, options);
|
|
63
|
+
return this.executeRequest(url, init);
|
|
64
|
+
}
|
|
65
|
+
/** Multi-turn completion: native message history support */
|
|
66
|
+
async messages(messages, options) {
|
|
67
|
+
const { url, init } = this.buildMessagesRequest(messages, options);
|
|
68
|
+
return this.executeRequest(url, init);
|
|
69
|
+
}
|
|
70
|
+
/** Streaming completion: yields text chunks */
|
|
71
|
+
async *stream(prompt, options) {
|
|
72
|
+
const { url, init } = this.buildRequest(prompt, { ...options, stream: true });
|
|
73
|
+
yield* this.executeStream(url, init);
|
|
74
|
+
}
|
|
75
|
+
/** Streaming with full message history */
|
|
76
|
+
async *streamMessages(messages, options) {
|
|
77
|
+
const { url, init } = this.buildMessagesRequest(messages, { ...options, stream: true });
|
|
78
|
+
yield* this.executeStream(url, init);
|
|
79
|
+
}
|
|
80
|
+
async *executeStream(url, init) {
|
|
81
|
+
const start = Date.now();
|
|
82
|
+
this.logger.info("llm_stream_start", { vendor: this.vendor, model: this.model });
|
|
83
|
+
const response = await this.fetchWithTimeout(url, init);
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
const body = await safeReadText(response);
|
|
86
|
+
this.logger.error("llm_stream_failed", { vendor: this.vendor, status: response.status });
|
|
87
|
+
throw new LLMError(`${this.vendor} stream returned ${response.status}: ${body.slice(0, 200)}`, this.vendor, response.status);
|
|
88
|
+
}
|
|
89
|
+
if (!response.body) {
|
|
90
|
+
throw new LLMError(`${this.vendor} stream response has no body`, this.vendor);
|
|
91
|
+
}
|
|
92
|
+
const reader = response.body.getReader();
|
|
93
|
+
const decoder = new TextDecoder();
|
|
94
|
+
let buffer = "";
|
|
95
|
+
try {
|
|
96
|
+
while (true) {
|
|
97
|
+
const { done, value } = await reader.read();
|
|
98
|
+
if (done) break;
|
|
99
|
+
buffer += decoder.decode(value, { stream: true });
|
|
100
|
+
const lines = buffer.split("\n");
|
|
101
|
+
buffer = lines.pop() ?? "";
|
|
102
|
+
for (const line of lines) {
|
|
103
|
+
const chunk = this.parseStreamLine(line);
|
|
104
|
+
if (chunk) yield chunk;
|
|
105
|
+
if (chunk?.done) {
|
|
106
|
+
this.logger.info("llm_stream_done", {
|
|
107
|
+
vendor: this.vendor,
|
|
108
|
+
duration: Date.now() - start,
|
|
109
|
+
finishReason: chunk.finishReason
|
|
110
|
+
});
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (buffer.trim()) {
|
|
116
|
+
const chunk = this.parseStreamLine(buffer);
|
|
117
|
+
if (chunk) yield chunk;
|
|
118
|
+
}
|
|
119
|
+
} finally {
|
|
120
|
+
reader.releaseLock();
|
|
121
|
+
}
|
|
122
|
+
yield { text: "", done: true };
|
|
123
|
+
this.logger.info("llm_stream_done", { vendor: this.vendor, duration: Date.now() - start });
|
|
124
|
+
}
|
|
125
|
+
async executeRequest(url, init) {
|
|
126
|
+
const start = Date.now();
|
|
127
|
+
this.logger.info("llm_request_start", {
|
|
128
|
+
vendor: this.vendor,
|
|
129
|
+
model: this.model,
|
|
130
|
+
timeoutMs: this.timeoutMs
|
|
131
|
+
});
|
|
132
|
+
const response = await this.fetchWithRetry(url, init);
|
|
133
|
+
const duration = Date.now() - start;
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
const body = await safeReadText(response);
|
|
136
|
+
this.logger.error("llm_request_failed", {
|
|
137
|
+
vendor: this.vendor,
|
|
138
|
+
model: this.model,
|
|
139
|
+
status: response.status,
|
|
140
|
+
duration,
|
|
141
|
+
body: body.slice(0, 200)
|
|
142
|
+
});
|
|
143
|
+
throw new LLMError(
|
|
144
|
+
`${this.vendor} API returned ${response.status}: ${body.slice(0, 200)}`,
|
|
145
|
+
this.vendor,
|
|
146
|
+
response.status,
|
|
147
|
+
duration
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
let payload;
|
|
151
|
+
try {
|
|
152
|
+
payload = await response.json();
|
|
153
|
+
} catch {
|
|
154
|
+
this.logger.error("llm_invalid_json", { vendor: this.vendor, duration });
|
|
155
|
+
throw new LLMError(`${this.vendor} response is not valid JSON`, this.vendor, response.status, duration);
|
|
156
|
+
}
|
|
157
|
+
const result = this.parseResponse(payload);
|
|
158
|
+
this.logger.info("llm_request_done", {
|
|
159
|
+
vendor: this.vendor,
|
|
160
|
+
model: this.model,
|
|
161
|
+
duration,
|
|
162
|
+
finishReason: result.finishReason,
|
|
163
|
+
tokens: result.usage?.totalTokens,
|
|
164
|
+
hasToolCalls: Boolean(result.toolCalls?.length)
|
|
165
|
+
});
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
// ─── Retry logic ─────────────────────────────────────────────────────────
|
|
169
|
+
async fetchWithRetry(url, init) {
|
|
170
|
+
let lastError = null;
|
|
171
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
172
|
+
try {
|
|
173
|
+
const response = await this.fetchWithTimeout(url, init);
|
|
174
|
+
if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < this.maxRetries) {
|
|
175
|
+
const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt);
|
|
176
|
+
this.logger.warn("llm_retrying", {
|
|
177
|
+
vendor: this.vendor,
|
|
178
|
+
attempt: attempt + 1,
|
|
179
|
+
status: response.status,
|
|
180
|
+
delayMs: delay
|
|
181
|
+
});
|
|
182
|
+
await sleep(delay);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
return response;
|
|
186
|
+
} catch (err) {
|
|
187
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
188
|
+
if (err instanceof LLMTimeoutError || attempt >= this.maxRetries) {
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt);
|
|
192
|
+
this.logger.warn("llm_retry_on_error", {
|
|
193
|
+
vendor: this.vendor,
|
|
194
|
+
attempt: attempt + 1,
|
|
195
|
+
error: lastError.message,
|
|
196
|
+
delayMs: delay
|
|
197
|
+
});
|
|
198
|
+
await sleep(delay);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
throw lastError ?? new LLMError(`${this.vendor} request failed after retries`, this.vendor);
|
|
202
|
+
}
|
|
203
|
+
async fetchWithTimeout(url, init) {
|
|
204
|
+
const controller = new AbortController();
|
|
205
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
206
|
+
try {
|
|
207
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
208
|
+
} catch (err) {
|
|
209
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
210
|
+
throw new LLMTimeoutError(this.vendor, this.timeoutMs);
|
|
211
|
+
}
|
|
212
|
+
throw new LLMError(
|
|
213
|
+
`${this.vendor} network error: ${err instanceof Error ? err.message : "unknown"}`,
|
|
214
|
+
this.vendor
|
|
215
|
+
);
|
|
216
|
+
} finally {
|
|
217
|
+
clearTimeout(timer);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// ─── JSON parsing ────────────────────────────────────────────────────────
|
|
221
|
+
parseAndValidate(content, schema) {
|
|
222
|
+
const jsonStr = extractJsonObject(content);
|
|
223
|
+
let parsed;
|
|
224
|
+
try {
|
|
225
|
+
parsed = JSON.parse(jsonStr);
|
|
226
|
+
} catch {
|
|
227
|
+
this.logger.error("llm_invalid_output_json", {
|
|
228
|
+
vendor: this.vendor,
|
|
229
|
+
snippet: content.slice(0, 100)
|
|
230
|
+
});
|
|
231
|
+
throw new LLMError(`${this.vendor} output is not valid JSON: ${content.slice(0, 100)}`, this.vendor);
|
|
232
|
+
}
|
|
233
|
+
const result = schema.safeParse(parsed);
|
|
234
|
+
if (!result.success) {
|
|
235
|
+
const issues = result.error.issues.slice(0, 3).map((i) => `${i.path.join(".")}: ${i.message}`);
|
|
236
|
+
this.logger.error("llm_schema_validation_failed", {
|
|
237
|
+
vendor: this.vendor,
|
|
238
|
+
issues
|
|
239
|
+
});
|
|
240
|
+
throw new LLMError(`${this.vendor} output failed schema validation: ${issues.join("; ")}`, this.vendor);
|
|
241
|
+
}
|
|
242
|
+
return result.data;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
function extractJsonObject(raw) {
|
|
246
|
+
const stripped = stripMarkdownFence(raw.trim());
|
|
247
|
+
const first = stripped.indexOf("{");
|
|
248
|
+
const last = stripped.lastIndexOf("}");
|
|
249
|
+
if (first === -1 || last === -1 || last < first) return stripped;
|
|
250
|
+
return stripped.slice(first, last + 1);
|
|
251
|
+
}
|
|
252
|
+
function stripMarkdownFence(raw) {
|
|
253
|
+
if (!raw.startsWith("```")) return raw;
|
|
254
|
+
const match = raw.match(/^```(?:json|JSON)?\s*\n?([\s\S]*?)\n?```$/);
|
|
255
|
+
return match?.[1]?.trim() ?? raw;
|
|
256
|
+
}
|
|
257
|
+
async function safeReadText(res) {
|
|
258
|
+
try {
|
|
259
|
+
return await res.text();
|
|
260
|
+
} catch {
|
|
261
|
+
return "";
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function sleep(ms) {
|
|
265
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/openai-provider.ts
|
|
269
|
+
var DEFAULT_TEMPERATURE = 0.7;
|
|
270
|
+
var OpenAIProvider = class extends BaseLLMProvider {
|
|
271
|
+
constructor(config) {
|
|
272
|
+
super({
|
|
273
|
+
apiKey: config.apiKey,
|
|
274
|
+
baseUrl: config.baseUrl,
|
|
275
|
+
model: config.model,
|
|
276
|
+
timeoutMs: config.timeoutMs,
|
|
277
|
+
vendor: config.vendor ?? "openai",
|
|
278
|
+
logger: config.logger,
|
|
279
|
+
maxRetries: config.maxRetries
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
getDefaultBaseUrl() {
|
|
283
|
+
return "https://api.openai.com/v1";
|
|
284
|
+
}
|
|
285
|
+
getDefaultModel() {
|
|
286
|
+
return "gpt-4o-mini";
|
|
287
|
+
}
|
|
288
|
+
getVendorName() {
|
|
289
|
+
return "openai";
|
|
290
|
+
}
|
|
291
|
+
buildRequest(prompt, options) {
|
|
292
|
+
const messages = [
|
|
293
|
+
{ role: "system", content: prompt.system },
|
|
294
|
+
{ role: "user", content: prompt.user }
|
|
295
|
+
];
|
|
296
|
+
return this.buildChatRequest(messages, options);
|
|
297
|
+
}
|
|
298
|
+
buildMessagesRequest(messages, options) {
|
|
299
|
+
const formatted = messages.map((m) => {
|
|
300
|
+
const msg = { role: m.role, content: m.content };
|
|
301
|
+
if (m.name) msg.name = m.name;
|
|
302
|
+
if (m.tool_call_id) msg.tool_call_id = m.tool_call_id;
|
|
303
|
+
return msg;
|
|
304
|
+
});
|
|
305
|
+
return this.buildChatRequest(formatted, options);
|
|
306
|
+
}
|
|
307
|
+
buildChatRequest(messages, options) {
|
|
308
|
+
const url = `${this.baseUrl}/chat/completions`;
|
|
309
|
+
const body = {
|
|
310
|
+
model: this.model,
|
|
311
|
+
messages,
|
|
312
|
+
temperature: options?.temperature ?? DEFAULT_TEMPERATURE
|
|
313
|
+
};
|
|
314
|
+
if (options?.maxTokens) {
|
|
315
|
+
body.max_tokens = options.maxTokens;
|
|
316
|
+
}
|
|
317
|
+
if (options?.jsonMode) {
|
|
318
|
+
body.response_format = { type: "json_object" };
|
|
319
|
+
}
|
|
320
|
+
if (options?.tools && options.tools.length > 0) {
|
|
321
|
+
body.tools = options.tools.map((t) => ({
|
|
322
|
+
type: "function",
|
|
323
|
+
function: {
|
|
324
|
+
name: t.name,
|
|
325
|
+
description: t.description,
|
|
326
|
+
parameters: t.parameters
|
|
327
|
+
}
|
|
328
|
+
}));
|
|
329
|
+
}
|
|
330
|
+
if (options?.stream) {
|
|
331
|
+
body.stream = true;
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
url,
|
|
335
|
+
init: {
|
|
336
|
+
method: "POST",
|
|
337
|
+
headers: {
|
|
338
|
+
"Content-Type": "application/json",
|
|
339
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
340
|
+
},
|
|
341
|
+
body: JSON.stringify(body)
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
parseResponse(payload) {
|
|
346
|
+
const data = payload;
|
|
347
|
+
const choice = data.choices?.[0];
|
|
348
|
+
const content = choice?.message?.content ?? "";
|
|
349
|
+
const finishReason = choice?.finish_reason;
|
|
350
|
+
let toolCalls;
|
|
351
|
+
if (choice?.message?.tool_calls && choice.message.tool_calls.length > 0) {
|
|
352
|
+
toolCalls = choice.message.tool_calls.map((tc) => ({
|
|
353
|
+
id: tc.id,
|
|
354
|
+
name: tc.function.name,
|
|
355
|
+
arguments: tc.function.arguments
|
|
356
|
+
}));
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
content,
|
|
360
|
+
toolCalls,
|
|
361
|
+
finishReason,
|
|
362
|
+
usage: data.usage ? {
|
|
363
|
+
promptTokens: data.usage.prompt_tokens,
|
|
364
|
+
completionTokens: data.usage.completion_tokens,
|
|
365
|
+
totalTokens: data.usage.total_tokens
|
|
366
|
+
} : void 0
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
parseStreamLine(line) {
|
|
370
|
+
if (!line.startsWith("data: ")) return null;
|
|
371
|
+
const data = line.slice(6).trim();
|
|
372
|
+
if (data === "[DONE]") return { text: "", done: true };
|
|
373
|
+
try {
|
|
374
|
+
const json = JSON.parse(data);
|
|
375
|
+
const choice = json.choices?.[0];
|
|
376
|
+
if (!choice) return null;
|
|
377
|
+
const delta = choice.delta;
|
|
378
|
+
const text = delta?.content ?? "";
|
|
379
|
+
const finishReason = choice.finish_reason ?? void 0;
|
|
380
|
+
const done = finishReason === "stop" || finishReason === "tool_calls";
|
|
381
|
+
let toolCall;
|
|
382
|
+
if (delta?.tool_calls?.[0]) {
|
|
383
|
+
const tc = delta.tool_calls[0];
|
|
384
|
+
toolCall = {
|
|
385
|
+
id: tc.id,
|
|
386
|
+
name: tc.function?.name,
|
|
387
|
+
argumentsDelta: tc.function?.arguments ?? ""
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
text,
|
|
392
|
+
toolCall,
|
|
393
|
+
done,
|
|
394
|
+
finishReason: done ? finishReason : void 0,
|
|
395
|
+
usage: json.usage ? {
|
|
396
|
+
promptTokens: json.usage.prompt_tokens,
|
|
397
|
+
completionTokens: json.usage.completion_tokens,
|
|
398
|
+
totalTokens: json.usage.total_tokens
|
|
399
|
+
} : void 0
|
|
400
|
+
};
|
|
401
|
+
} catch {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
// src/anthropic-provider.ts
|
|
408
|
+
var DEFAULT_MAX_TOKENS = 4096;
|
|
409
|
+
var AnthropicProvider = class extends BaseLLMProvider {
|
|
410
|
+
constructor(config) {
|
|
411
|
+
super({
|
|
412
|
+
apiKey: config.apiKey,
|
|
413
|
+
baseUrl: config.baseUrl,
|
|
414
|
+
model: config.model,
|
|
415
|
+
timeoutMs: config.timeoutMs,
|
|
416
|
+
vendor: config.vendor ?? "anthropic",
|
|
417
|
+
logger: config.logger,
|
|
418
|
+
maxRetries: config.maxRetries
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
getDefaultBaseUrl() {
|
|
422
|
+
return "https://api.anthropic.com";
|
|
423
|
+
}
|
|
424
|
+
getDefaultModel() {
|
|
425
|
+
return "claude-sonnet-4-20250514";
|
|
426
|
+
}
|
|
427
|
+
getVendorName() {
|
|
428
|
+
return "anthropic";
|
|
429
|
+
}
|
|
430
|
+
buildRequest(prompt, options) {
|
|
431
|
+
const messages = [{ role: "user", content: prompt.user }];
|
|
432
|
+
return this.buildAnthropicRequest(prompt.system, messages, options);
|
|
433
|
+
}
|
|
434
|
+
buildMessagesRequest(messages, options) {
|
|
435
|
+
const system = messages.find((m) => m.role === "system")?.content ?? "";
|
|
436
|
+
const nonSystem = messages.filter((m) => m.role !== "system").map((m) => {
|
|
437
|
+
const msg = { role: m.role, content: m.content };
|
|
438
|
+
if (m.name) msg.name = m.name;
|
|
439
|
+
if (m.tool_call_id) msg.tool_call_id = m.tool_call_id;
|
|
440
|
+
return msg;
|
|
441
|
+
});
|
|
442
|
+
return this.buildAnthropicRequest(system, nonSystem, options);
|
|
443
|
+
}
|
|
444
|
+
buildAnthropicRequest(system, messages, options) {
|
|
445
|
+
const url = `${this.baseUrl}/v1/messages`;
|
|
446
|
+
const body = {
|
|
447
|
+
model: this.model,
|
|
448
|
+
max_tokens: options?.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
449
|
+
system,
|
|
450
|
+
messages
|
|
451
|
+
};
|
|
452
|
+
if (options?.temperature !== void 0) {
|
|
453
|
+
body.temperature = options.temperature;
|
|
454
|
+
}
|
|
455
|
+
if (options?.tools && options.tools.length > 0) {
|
|
456
|
+
body.tools = options.tools.map((t) => ({
|
|
457
|
+
name: t.name,
|
|
458
|
+
description: t.description,
|
|
459
|
+
input_schema: t.parameters
|
|
460
|
+
}));
|
|
461
|
+
}
|
|
462
|
+
if (options?.stream) {
|
|
463
|
+
body.stream = true;
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
url,
|
|
467
|
+
init: {
|
|
468
|
+
method: "POST",
|
|
469
|
+
headers: {
|
|
470
|
+
"Content-Type": "application/json",
|
|
471
|
+
"x-api-key": this.apiKey,
|
|
472
|
+
"anthropic-version": "2023-06-01"
|
|
473
|
+
},
|
|
474
|
+
body: JSON.stringify(body)
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
parseResponse(payload) {
|
|
479
|
+
const data = payload;
|
|
480
|
+
const textBlocks = data.content?.filter((b) => b.type === "text") ?? [];
|
|
481
|
+
const content = textBlocks.map((b) => b.text ?? "").join("");
|
|
482
|
+
let toolCalls;
|
|
483
|
+
const toolBlocks = data.content?.filter((b) => b.type === "tool_use") ?? [];
|
|
484
|
+
if (toolBlocks.length > 0) {
|
|
485
|
+
toolCalls = toolBlocks.map((b) => ({
|
|
486
|
+
id: b.id ?? "",
|
|
487
|
+
name: b.name ?? "",
|
|
488
|
+
arguments: JSON.stringify(b.input ?? {})
|
|
489
|
+
}));
|
|
490
|
+
}
|
|
491
|
+
return {
|
|
492
|
+
content,
|
|
493
|
+
toolCalls,
|
|
494
|
+
finishReason: data.stop_reason,
|
|
495
|
+
usage: data.usage ? {
|
|
496
|
+
promptTokens: data.usage.input_tokens,
|
|
497
|
+
completionTokens: data.usage.output_tokens,
|
|
498
|
+
totalTokens: (data.usage.input_tokens ?? 0) + (data.usage.output_tokens ?? 0)
|
|
499
|
+
} : void 0
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
parseStreamLine(line) {
|
|
503
|
+
if (!line.startsWith("data: ")) return null;
|
|
504
|
+
const data = line.slice(6).trim();
|
|
505
|
+
if (!data) return null;
|
|
506
|
+
try {
|
|
507
|
+
const json = JSON.parse(data);
|
|
508
|
+
switch (json.type) {
|
|
509
|
+
case "content_block_delta": {
|
|
510
|
+
const delta = json.delta;
|
|
511
|
+
if (delta?.type === "text_delta") {
|
|
512
|
+
return { text: delta.text ?? "", done: false };
|
|
513
|
+
}
|
|
514
|
+
if (delta?.type === "input_json_delta") {
|
|
515
|
+
return { text: "", toolCall: { argumentsDelta: delta.partial_json ?? "" }, done: false };
|
|
516
|
+
}
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
case "content_block_start": {
|
|
520
|
+
if (json.content_block?.type === "tool_use") {
|
|
521
|
+
return {
|
|
522
|
+
text: "",
|
|
523
|
+
toolCall: { id: json.content_block.id, name: json.content_block.name, argumentsDelta: "" },
|
|
524
|
+
done: false
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
case "message_delta": {
|
|
530
|
+
return {
|
|
531
|
+
text: "",
|
|
532
|
+
done: json.delta?.stop_reason === "end_turn" || json.delta?.stop_reason === "tool_use",
|
|
533
|
+
finishReason: json.delta?.stop_reason ?? void 0,
|
|
534
|
+
usage: json.usage ? {
|
|
535
|
+
promptTokens: json.usage.input_tokens,
|
|
536
|
+
completionTokens: json.usage.output_tokens,
|
|
537
|
+
totalTokens: (json.usage.input_tokens ?? 0) + (json.usage.output_tokens ?? 0)
|
|
538
|
+
} : void 0
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
case "message_stop":
|
|
542
|
+
return { text: "", done: true };
|
|
543
|
+
default:
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
} catch {
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
// src/tool-executor.ts
|
|
553
|
+
var DEFAULT_MAX_ROUNDS = 5;
|
|
554
|
+
var ToolExecutor = class {
|
|
555
|
+
constructor(options) {
|
|
556
|
+
this.tools = /* @__PURE__ */ new Map();
|
|
557
|
+
this.logger = options?.logger ?? noopLogger;
|
|
558
|
+
}
|
|
559
|
+
register(tools) {
|
|
560
|
+
for (const tool of tools) {
|
|
561
|
+
this.tools.set(tool.definition.name, tool);
|
|
562
|
+
this.logger.info("tool_registered", { name: tool.definition.name });
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
async completeWithTools(provider, prompt, schema, options) {
|
|
566
|
+
const maxRounds = options?.maxRounds ?? DEFAULT_MAX_ROUNDS;
|
|
567
|
+
const toolDefs = Array.from(this.tools.values()).map((t) => t.definition);
|
|
568
|
+
const messages = [
|
|
569
|
+
{ role: "system", content: prompt.system },
|
|
570
|
+
{ role: "user", content: prompt.user }
|
|
571
|
+
];
|
|
572
|
+
let round = 0;
|
|
573
|
+
while (round <= maxRounds) {
|
|
574
|
+
round++;
|
|
575
|
+
this.logger.info("tool_round_start", { round, messageCount: messages.length });
|
|
576
|
+
const result = await this.callWithMessages(provider, messages, toolDefs);
|
|
577
|
+
if (!result.toolCalls || result.toolCalls.length === 0) {
|
|
578
|
+
this.logger.info("tool_round_final", { round, contentLength: result.content.length });
|
|
579
|
+
return this.validateOutput(result.content, schema);
|
|
580
|
+
}
|
|
581
|
+
this.logger.info("tool_calls_requested", {
|
|
582
|
+
round,
|
|
583
|
+
toolCount: result.toolCalls.length,
|
|
584
|
+
tools: result.toolCalls.map((tc) => tc.name)
|
|
585
|
+
});
|
|
586
|
+
messages.push({
|
|
587
|
+
role: "assistant",
|
|
588
|
+
content: result.content || "",
|
|
589
|
+
toolCalls: result.toolCalls
|
|
590
|
+
});
|
|
591
|
+
for (const call of result.toolCalls) {
|
|
592
|
+
const output = await this.executeTool(call);
|
|
593
|
+
messages.push({
|
|
594
|
+
role: "tool",
|
|
595
|
+
content: output,
|
|
596
|
+
toolCallId: call.id,
|
|
597
|
+
name: call.name
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
throw new LLMError(
|
|
602
|
+
`Tool call loop exceeded ${maxRounds} rounds without final response`,
|
|
603
|
+
"tool-executor"
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
// ─── Private methods ─────────────────────────────────────────────────────
|
|
607
|
+
async executeTool(call) {
|
|
608
|
+
const registration = this.tools.get(call.name);
|
|
609
|
+
if (!registration) {
|
|
610
|
+
this.logger.warn("tool_not_found", { name: call.name });
|
|
611
|
+
return JSON.stringify({ error: `Unknown tool: ${call.name}` });
|
|
612
|
+
}
|
|
613
|
+
try {
|
|
614
|
+
const args = JSON.parse(call.arguments);
|
|
615
|
+
this.logger.info("tool_executing", { name: call.name, callId: call.id });
|
|
616
|
+
const output = await registration.handler(args);
|
|
617
|
+
this.logger.info("tool_executed", { name: call.name, outputLength: output.length });
|
|
618
|
+
return output;
|
|
619
|
+
} catch (err) {
|
|
620
|
+
const msg = err instanceof Error ? err.message : "Tool execution failed";
|
|
621
|
+
this.logger.error("tool_execution_error", { name: call.name, error: msg });
|
|
622
|
+
return JSON.stringify({ error: msg });
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Call LLM with native multi-turn messages API.
|
|
627
|
+
*/
|
|
628
|
+
async callWithMessages(provider, messages, tools) {
|
|
629
|
+
const llmMessages = messages.map((m) => ({
|
|
630
|
+
role: m.role,
|
|
631
|
+
content: m.content,
|
|
632
|
+
name: m.name,
|
|
633
|
+
tool_call_id: m.toolCallId
|
|
634
|
+
}));
|
|
635
|
+
return provider.messages(llmMessages, { jsonMode: true, tools });
|
|
636
|
+
}
|
|
637
|
+
validateOutput(content, schema) {
|
|
638
|
+
if (!content) {
|
|
639
|
+
throw new LLMError("LLM returned empty content after tool calls", "tool-executor");
|
|
640
|
+
}
|
|
641
|
+
const jsonStr = extractJson(content);
|
|
642
|
+
let parsed;
|
|
643
|
+
try {
|
|
644
|
+
parsed = JSON.parse(jsonStr);
|
|
645
|
+
} catch {
|
|
646
|
+
throw new LLMError(`Final output is not valid JSON: ${content.slice(0, 100)}`, "tool-executor");
|
|
647
|
+
}
|
|
648
|
+
const validated = schema.safeParse(parsed);
|
|
649
|
+
if (!validated.success) {
|
|
650
|
+
const issue = validated.error.issues[0];
|
|
651
|
+
throw new LLMError(
|
|
652
|
+
`Final output failed schema validation: ${issue?.path.join(".")}: ${issue?.message}`,
|
|
653
|
+
"tool-executor"
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
return validated.data;
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
function extractJson(raw) {
|
|
660
|
+
const trimmed = raw.trim();
|
|
661
|
+
if (trimmed.startsWith("```")) {
|
|
662
|
+
const match = trimmed.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```$/);
|
|
663
|
+
if (match?.[1]) return match[1].trim();
|
|
664
|
+
}
|
|
665
|
+
const first = trimmed.indexOf("{");
|
|
666
|
+
const last = trimmed.lastIndexOf("}");
|
|
667
|
+
if (first !== -1 && last > first) return trimmed.slice(first, last + 1);
|
|
668
|
+
return trimmed;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// src/mcp-client.ts
|
|
672
|
+
var MCPClient = class {
|
|
673
|
+
constructor() {
|
|
674
|
+
this.servers = /* @__PURE__ */ new Map();
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Connect to an MCP server and discover its tools.
|
|
678
|
+
* Returns the number of tools discovered.
|
|
679
|
+
*/
|
|
680
|
+
async connect(config) {
|
|
681
|
+
const serverName = config.name ?? new URL(config.url).hostname;
|
|
682
|
+
const tools = await this.discoverTools(config);
|
|
683
|
+
this.servers.set(serverName, { config, tools });
|
|
684
|
+
return tools.length;
|
|
685
|
+
}
|
|
686
|
+
/** Disconnect from a server */
|
|
687
|
+
disconnect(name) {
|
|
688
|
+
this.servers.delete(name);
|
|
689
|
+
}
|
|
690
|
+
/** Refresh tool list from a connected server (re-discovery) */
|
|
691
|
+
async refresh(name) {
|
|
692
|
+
const state = this.servers.get(name);
|
|
693
|
+
if (!state) {
|
|
694
|
+
throw new LLMError(`MCP server '${name}' is not connected`, "mcp");
|
|
695
|
+
}
|
|
696
|
+
const tools = await this.discoverTools(state.config);
|
|
697
|
+
this.servers.set(name, { config: state.config, tools });
|
|
698
|
+
return tools.length;
|
|
699
|
+
}
|
|
700
|
+
/** Health check: ping a connected server */
|
|
701
|
+
async healthCheck(name) {
|
|
702
|
+
const state = this.servers.get(name);
|
|
703
|
+
if (!state) return false;
|
|
704
|
+
try {
|
|
705
|
+
await this.rpcCall(state.config, "ping", {});
|
|
706
|
+
return true;
|
|
707
|
+
} catch {
|
|
708
|
+
return false;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
/** Get all connected server names */
|
|
712
|
+
getServerNames() {
|
|
713
|
+
return Array.from(this.servers.keys());
|
|
714
|
+
}
|
|
715
|
+
/** Get all tool definitions from all connected servers */
|
|
716
|
+
getAllTools() {
|
|
717
|
+
const tools = [];
|
|
718
|
+
for (const state of this.servers.values()) {
|
|
719
|
+
for (const tool of state.tools) {
|
|
720
|
+
tools.push({
|
|
721
|
+
name: tool.name,
|
|
722
|
+
description: tool.description ?? "",
|
|
723
|
+
parameters: tool.inputSchema ?? { type: "object", properties: {} }
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return tools;
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Get tool registrations ready for ToolExecutor.register().
|
|
731
|
+
* Each tool's handler calls the MCP server via HTTP.
|
|
732
|
+
*/
|
|
733
|
+
getToolRegistrations() {
|
|
734
|
+
const registrations = [];
|
|
735
|
+
for (const state of this.servers.values()) {
|
|
736
|
+
for (const tool of state.tools) {
|
|
737
|
+
const definition = {
|
|
738
|
+
name: tool.name,
|
|
739
|
+
description: tool.description ?? "",
|
|
740
|
+
parameters: tool.inputSchema ?? { type: "object", properties: {} }
|
|
741
|
+
};
|
|
742
|
+
const handler = async (args) => {
|
|
743
|
+
return this.callTool(state.config, tool.name, args);
|
|
744
|
+
};
|
|
745
|
+
registrations.push({ definition, handler });
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return registrations;
|
|
749
|
+
}
|
|
750
|
+
// ─── Private methods ─────────────────────────────────────────────────────
|
|
751
|
+
/** Discover tools from MCP server */
|
|
752
|
+
async discoverTools(config) {
|
|
753
|
+
const response = await this.rpcCall(config, "tools/list", {});
|
|
754
|
+
const data = response;
|
|
755
|
+
return data.tools ?? [];
|
|
756
|
+
}
|
|
757
|
+
/** Call a tool on the MCP server */
|
|
758
|
+
async callTool(config, toolName, args) {
|
|
759
|
+
const response = await this.rpcCall(config, "tools/call", {
|
|
760
|
+
name: toolName,
|
|
761
|
+
arguments: args
|
|
762
|
+
});
|
|
763
|
+
const result = response;
|
|
764
|
+
if (result.isError) {
|
|
765
|
+
const errorText = result.content?.find((c) => c.type === "text")?.text ?? "Tool call failed";
|
|
766
|
+
throw new LLMError(`MCP tool '${toolName}' returned error: ${errorText}`, "mcp");
|
|
767
|
+
}
|
|
768
|
+
const textParts = result.content?.filter((c) => c.type === "text").map((c) => c.text ?? "") ?? [];
|
|
769
|
+
return textParts.join("\n") || JSON.stringify(result);
|
|
770
|
+
}
|
|
771
|
+
/** Send JSON-RPC request to MCP server */
|
|
772
|
+
async rpcCall(config, method, params) {
|
|
773
|
+
const timeout = config.timeoutMs ?? 15e3;
|
|
774
|
+
const controller = new AbortController();
|
|
775
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
776
|
+
const headers = {
|
|
777
|
+
"Content-Type": "application/json"
|
|
778
|
+
};
|
|
779
|
+
if (config.authToken) {
|
|
780
|
+
headers["Authorization"] = `Bearer ${config.authToken}`;
|
|
781
|
+
}
|
|
782
|
+
const body = JSON.stringify({
|
|
783
|
+
jsonrpc: "2.0",
|
|
784
|
+
id: crypto.randomUUID(),
|
|
785
|
+
method,
|
|
786
|
+
params
|
|
787
|
+
});
|
|
788
|
+
try {
|
|
789
|
+
const response = await fetch(config.url, {
|
|
790
|
+
method: "POST",
|
|
791
|
+
headers,
|
|
792
|
+
body,
|
|
793
|
+
signal: controller.signal
|
|
794
|
+
});
|
|
795
|
+
if (!response.ok) {
|
|
796
|
+
const text = await response.text().catch(() => "");
|
|
797
|
+
throw new LLMError(
|
|
798
|
+
`MCP server '${config.name ?? config.url}' returned ${response.status}: ${text.slice(0, 200)}`,
|
|
799
|
+
"mcp",
|
|
800
|
+
response.status
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
const json = await response.json();
|
|
804
|
+
if (json.error) {
|
|
805
|
+
throw new LLMError(`MCP RPC error: ${json.error.message}`, "mcp");
|
|
806
|
+
}
|
|
807
|
+
return json.result;
|
|
808
|
+
} catch (err) {
|
|
809
|
+
if (err instanceof LLMError) throw err;
|
|
810
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
811
|
+
throw new LLMError(`MCP server '${config.name ?? config.url}' timed out after ${timeout}ms`, "mcp");
|
|
812
|
+
}
|
|
813
|
+
throw new LLMError(
|
|
814
|
+
`MCP connection failed: ${err instanceof Error ? err.message : "unknown"}`,
|
|
815
|
+
"mcp"
|
|
816
|
+
);
|
|
817
|
+
} finally {
|
|
818
|
+
clearTimeout(timer);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
// src/skill-loader.ts
|
|
824
|
+
var SkillLoader = class {
|
|
825
|
+
constructor() {
|
|
826
|
+
this.skills = /* @__PURE__ */ new Map();
|
|
827
|
+
}
|
|
828
|
+
/** Add a skill from raw SKILL.md content */
|
|
829
|
+
add(id, rawContent) {
|
|
830
|
+
const { name, description, body } = parseFrontmatter(rawContent);
|
|
831
|
+
this.skills.set(id, { id, name: name || id, description: description || "", body });
|
|
832
|
+
return this;
|
|
833
|
+
}
|
|
834
|
+
/** Add a skill from pre-parsed entry */
|
|
835
|
+
addEntry(entry) {
|
|
836
|
+
this.skills.set(entry.id, entry);
|
|
837
|
+
return this;
|
|
838
|
+
}
|
|
839
|
+
/** Remove a skill by id */
|
|
840
|
+
remove(id) {
|
|
841
|
+
this.skills.delete(id);
|
|
842
|
+
return this;
|
|
843
|
+
}
|
|
844
|
+
/** Check if a skill is loaded */
|
|
845
|
+
has(id) {
|
|
846
|
+
return this.skills.has(id);
|
|
847
|
+
}
|
|
848
|
+
/** Get loaded skill ids */
|
|
849
|
+
ids() {
|
|
850
|
+
return Array.from(this.skills.keys());
|
|
851
|
+
}
|
|
852
|
+
/** Clear all loaded skills */
|
|
853
|
+
clear() {
|
|
854
|
+
this.skills.clear();
|
|
855
|
+
return this;
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Assemble all loaded skills into a system prompt string.
|
|
859
|
+
*
|
|
860
|
+
* Format:
|
|
861
|
+
* ```
|
|
862
|
+
* {preamble}
|
|
863
|
+
*
|
|
864
|
+
* === SKILL: {name} ===
|
|
865
|
+
* {body}
|
|
866
|
+
*
|
|
867
|
+
* === SKILL: {name2} ===
|
|
868
|
+
* {body2}
|
|
869
|
+
*
|
|
870
|
+
* {postamble}
|
|
871
|
+
* ```
|
|
872
|
+
*/
|
|
873
|
+
toSystemPrompt(options) {
|
|
874
|
+
const parts = [];
|
|
875
|
+
if (options?.preamble) {
|
|
876
|
+
parts.push(options.preamble);
|
|
877
|
+
}
|
|
878
|
+
for (const skill of this.skills.values()) {
|
|
879
|
+
const section = `
|
|
880
|
+
=== SKILL: ${skill.name} ===
|
|
881
|
+
${skill.body}`;
|
|
882
|
+
parts.push(section);
|
|
883
|
+
}
|
|
884
|
+
if (options?.postamble) {
|
|
885
|
+
parts.push(`
|
|
886
|
+
${options.postamble}`);
|
|
887
|
+
}
|
|
888
|
+
let result = parts.join("\n");
|
|
889
|
+
if (options?.maxChars && result.length > options.maxChars) {
|
|
890
|
+
result = result.slice(0, options.maxChars) + "\n\n[... truncated due to length limit]";
|
|
891
|
+
}
|
|
892
|
+
return result;
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Build a complete LLMPrompt with skills as system context.
|
|
896
|
+
*
|
|
897
|
+
* Convenience method that combines skill system prompt with user message.
|
|
898
|
+
*/
|
|
899
|
+
buildPrompt(userMessage, options) {
|
|
900
|
+
return {
|
|
901
|
+
system: this.toSystemPrompt(options),
|
|
902
|
+
user: userMessage
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
function parseFrontmatter(raw) {
|
|
907
|
+
const trimmed = raw.trim();
|
|
908
|
+
if (!trimmed.startsWith("---")) {
|
|
909
|
+
return { name: "", description: "", body: trimmed };
|
|
910
|
+
}
|
|
911
|
+
const endIndex = trimmed.indexOf("---", 3);
|
|
912
|
+
if (endIndex === -1) {
|
|
913
|
+
return { name: "", description: "", body: trimmed };
|
|
914
|
+
}
|
|
915
|
+
const yamlBlock = trimmed.slice(3, endIndex).trim();
|
|
916
|
+
const body = trimmed.slice(endIndex + 3).trim();
|
|
917
|
+
let name = "";
|
|
918
|
+
let description = "";
|
|
919
|
+
for (const line of yamlBlock.split("\n")) {
|
|
920
|
+
const nameMatch = line.match(/^name:\s*(.+)/);
|
|
921
|
+
if (nameMatch && nameMatch[1]) {
|
|
922
|
+
name = nameMatch[1].trim().replace(/^['"]|['"]$/g, "");
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
const descMatch = line.match(/^description:\s*(.+)/);
|
|
926
|
+
if (descMatch && descMatch[1]) {
|
|
927
|
+
description = descMatch[1].trim().replace(/^['"]|['"]$/g, "");
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
if (line.match(/^description:\s*[|>]/)) {
|
|
931
|
+
const descLines = [];
|
|
932
|
+
const allLines = yamlBlock.split("\n");
|
|
933
|
+
const startIdx = allLines.indexOf(line);
|
|
934
|
+
for (let i = startIdx + 1; i < allLines.length; i++) {
|
|
935
|
+
const currentLine = allLines[i];
|
|
936
|
+
if (currentLine && (currentLine.startsWith(" ") || currentLine.startsWith(" "))) {
|
|
937
|
+
descLines.push(currentLine.trim());
|
|
938
|
+
} else {
|
|
939
|
+
break;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
description = descLines.join(" ");
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
return { name, description, body };
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// src/index.ts
|
|
949
|
+
function createProvider(options) {
|
|
950
|
+
const { provider = "openai", ...config } = options;
|
|
951
|
+
switch (provider) {
|
|
952
|
+
case "anthropic":
|
|
953
|
+
return new AnthropicProvider(config);
|
|
954
|
+
case "openai":
|
|
955
|
+
default:
|
|
956
|
+
return new OpenAIProvider(config);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
export {
|
|
960
|
+
AnthropicProvider,
|
|
961
|
+
LLMError,
|
|
962
|
+
LLMSchemaError,
|
|
963
|
+
LLMTimeoutError,
|
|
964
|
+
MCPClient,
|
|
965
|
+
OpenAIProvider,
|
|
966
|
+
SkillLoader,
|
|
967
|
+
ToolExecutor,
|
|
968
|
+
consoleLogger,
|
|
969
|
+
createProvider,
|
|
970
|
+
extractJsonObject,
|
|
971
|
+
noopLogger
|
|
972
|
+
};
|