natureco-cli 5.20.3 → 5.21.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/CHANGELOG.md +779 -750
- package/README.md +608 -608
- package/bin/natureco.js +1099 -1095
- package/package.json +95 -94
- package/scripts/generate-qr.js +21 -0
- package/scripts/import-curated-skills-log.json +5 -0
- package/scripts/import-curated-skills.js +262 -0
- package/scripts/import-skills-log.json +167 -0
- package/scripts/import-skills.js +261 -0
- package/scripts/postinstall.js +125 -0
- package/src/commands/agent.js +280 -280
- package/src/commands/ask.js +4 -2
- package/src/commands/naturehub.js +206 -282
- package/src/commands/repl.js +1594 -1580
- package/src/providers/mem0-memory.js +121 -121
- package/src/providers/supermemory-memory.js +117 -117
- package/src/tools/llm_task.js +150 -150
- package/src/tools/mac_alarm.js +199 -199
- package/src/tools/workflow.js +424 -424
- package/src/utils/api.js +1211 -1188
- package/src/utils/cost-tracker.js +361 -360
- package/src/utils/inquirer-wrapper.js +43 -31
- package/src/utils/paste-safe-input.js +346 -334
- package/src/utils/process-errors.js +129 -115
- package/src/utils/provider-detect.js +76 -72
- package/src/utils/skills.js +1 -1
- package/src/utils/system-prompt.js +136 -136
- package/src/utils/tools.js +324 -324
- package/README.md.bak +0 -565
- package/src/tools/http.js +0 -78
package/src/utils/api.js
CHANGED
|
@@ -1,1188 +1,1211 @@
|
|
|
1
|
-
// NatureCo CLI v2.10.1 - Universal LLM Provider Support + MCP Integration
|
|
2
|
-
// Supports: OpenAI, Groq, Together, Fireworks, Perplexity, Mistral, DeepSeek, OpenRouter, Ollama, LM Studio, Anthropic
|
|
3
|
-
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const os = require('os');
|
|
6
|
-
const path = require('path');
|
|
7
|
-
const chalk = require('chalk');
|
|
8
|
-
const { getConfig } = require('./config');
|
|
9
|
-
const { getToolDefinitions, executeToolCalls } = require('./tool-runner');
|
|
10
|
-
const { MCPClient } = require('./mcp-client');
|
|
11
|
-
const TB = require('./token-budget');
|
|
12
|
-
const { accumulateToolCallDeltas } = require('./streaming-tools');
|
|
13
|
-
const { ToolGuardrails } = require('./tool-guardrails');
|
|
14
|
-
const guardrails = new ToolGuardrails();
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* v5.5.0: Provider-specific format detection
|
|
18
|
-
* Groq, OpenAI, Anthropic, Mistral, DeepSeek, OpenRouter, Ollama, MiniMax
|
|
19
|
-
*
|
|
20
|
-
* Canonical implementation lives in src/utils/provider-detect.js;
|
|
21
|
-
* re-exported here so the historical `detectProvider` reference inside
|
|
22
|
-
* api.js continues to work without touching every call site.
|
|
23
|
-
*/
|
|
24
|
-
const { detectProvider, isMiniMax, isGemini } = require('./provider-detect');
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* v5.5.0: Tool definitions'ı provider'a göre normalize et
|
|
28
|
-
* - OpenAI/Groq/Mistral/DeepSeek/OpenRouter: tool_choice, function calling OK
|
|
29
|
-
* - Anthropic: tools, system ayrı, content array
|
|
30
|
-
* - Ollama: tool support sınırlı, genelde yok
|
|
31
|
-
* - Perplexity: tool support yok
|
|
32
|
-
*/
|
|
33
|
-
function normalizeToolsForProvider(tools, provider) {
|
|
34
|
-
if (!tools || tools.length === 0) return tools;
|
|
35
|
-
if (provider === 'ollama' || provider === 'perplexity') {
|
|
36
|
-
// Bu providerlar tool support etmiyor - bos dondur
|
|
37
|
-
return [];
|
|
38
|
-
}
|
|
39
|
-
if (provider === 'anthropic') {
|
|
40
|
-
// Anthropic tools format: { name, description, input_schema }
|
|
41
|
-
return tools.map(t => ({
|
|
42
|
-
name: t.function.name,
|
|
43
|
-
description: t.function.description,
|
|
44
|
-
input_schema: t.function.parameters || { type: 'object', properties: {} }
|
|
45
|
-
}));
|
|
46
|
-
}
|
|
47
|
-
// OpenAI-compatible (Groq, Mistral, DeepSeek, OpenRouter, MiniMax, Together, Fireworks)
|
|
48
|
-
return tools;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* v5.5.0: Tool call'ları provider'a göre parse et
|
|
53
|
-
* OpenAI: tool_calls[].function.arguments (string)
|
|
54
|
-
* Anthropic: content[].type=tool_use, input (object)
|
|
55
|
-
* Ollama: genelde yok
|
|
56
|
-
*/
|
|
57
|
-
function parseToolCallsFromResponse(message, provider) {
|
|
58
|
-
if (provider === 'anthropic') {
|
|
59
|
-
// Anthropic: content array içinde tool_use blokları var
|
|
60
|
-
if (Array.isArray(message.content)) {
|
|
61
|
-
const toolUses = message.content.filter(c => c.type === 'tool_use');
|
|
62
|
-
return toolUses.map(tu => ({
|
|
63
|
-
id: tu.id,
|
|
64
|
-
type: 'function',
|
|
65
|
-
function: {
|
|
66
|
-
name: tu.name,
|
|
67
|
-
arguments: JSON.stringify(tu.input || {})
|
|
68
|
-
}
|
|
69
|
-
}));
|
|
70
|
-
}
|
|
71
|
-
return [];
|
|
72
|
-
}
|
|
73
|
-
// OpenAI-compatible
|
|
74
|
-
return message.tool_calls || [];
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Anthropic Messages API requires `system` to be either a non-empty string
|
|
79
|
-
* or omitted entirely. Sending `undefined` (which JSON.stringify drops to
|
|
80
|
-
* silent absence) leaves the model unanchored; sending `''` returns 400
|
|
81
|
-
* "system: cannot be empty" on recent API revisions. Always pass a
|
|
82
|
-
* meaningful default when no system message is present.
|
|
83
|
-
*/
|
|
84
|
-
const DEFAULT_ANTHROPIC_SYSTEM =
|
|
85
|
-
'You are a helpful AI assistant running inside the natureco CLI.';
|
|
86
|
-
|
|
87
|
-
function extractSystemForAnthropic(messages) {
|
|
88
|
-
const systemMsg = messages.find(m => m.role === 'system');
|
|
89
|
-
if (!systemMsg) return DEFAULT_ANTHROPIC_SYSTEM;
|
|
90
|
-
// content may be a string or an array of content blocks; both round-trip.
|
|
91
|
-
if (typeof systemMsg.content === 'string') {
|
|
92
|
-
return systemMsg.content.trim() || DEFAULT_ANTHROPIC_SYSTEM;
|
|
93
|
-
}
|
|
94
|
-
if (Array.isArray(systemMsg.content) && systemMsg.content.length > 0) {
|
|
95
|
-
return systemMsg.content;
|
|
96
|
-
}
|
|
97
|
-
return DEFAULT_ANTHROPIC_SYSTEM;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* v5.5.0: System mesajı provider'a göre ayarla
|
|
102
|
-
* - OpenAI: messages[].role=system
|
|
103
|
-
* - Anthropic: ayrı 'system' field
|
|
104
|
-
*/
|
|
105
|
-
function buildRequestBody(messages, model, options, provider) {
|
|
106
|
-
if (provider === 'anthropic') {
|
|
107
|
-
const userMsgs = messages.filter(m => m.role !== 'system');
|
|
108
|
-
return {
|
|
109
|
-
model,
|
|
110
|
-
messages: userMsgs.map(m => ({
|
|
111
|
-
role: m.role,
|
|
112
|
-
content: m.content
|
|
113
|
-
})),
|
|
114
|
-
system: extractSystemForAnthropic(messages),
|
|
115
|
-
max_tokens: options.max_tokens || 4096,
|
|
116
|
-
temperature: options.temperature || 0.7,
|
|
117
|
-
...(options.tools && options.tools.length > 0 ? { tools: options.tools } : {})
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
// OpenAI-compatible
|
|
121
|
-
return {
|
|
122
|
-
model,
|
|
123
|
-
messages,
|
|
124
|
-
max_tokens: options.max_tokens || 4096,
|
|
125
|
-
temperature: options.temperature || 0.7,
|
|
126
|
-
...(options.tools && options.tools.length > 0 ? { tools: options.tools, tool_choice: 'auto' } : {})
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// Persistent conversation directory
|
|
132
|
-
const CONV_DIR = path.join(os.homedir(), '.natureco', 'conversations');
|
|
133
|
-
|
|
134
|
-
// Conversation history for multi-turn chat (deprecated - now using disk storage)
|
|
135
|
-
const conversationHistory = new Map();
|
|
136
|
-
|
|
137
|
-
// MCP clients (server name -> { client, tools })
|
|
138
|
-
const mcpClients = {};
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Generate default conversation ID based on provider config
|
|
142
|
-
*/
|
|
143
|
-
function generateDefaultConvId() {
|
|
144
|
-
const config = getConfig();
|
|
145
|
-
|
|
146
|
-
// Use provider URL + model as base for consistent ID
|
|
147
|
-
const providerUrl = config.providerUrl || 'default';
|
|
148
|
-
const model = config.providerModel || 'default';
|
|
149
|
-
|
|
150
|
-
// Create simple hash-like ID from provider + model
|
|
151
|
-
const base = `${providerUrl}_${model}`.replace(/[^a-z0-9]/gi, '_').toLowerCase();
|
|
152
|
-
|
|
153
|
-
// Return consistent ID (e.g., "groq_llama_3_1_8b_instant")
|
|
154
|
-
return base.slice(0, 50); // Limit length
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Load conversation from disk
|
|
159
|
-
*/
|
|
160
|
-
function loadConversation(convId) {
|
|
161
|
-
const file = path.join(CONV_DIR, `${convId.replace(/[^a-z0-9]/gi, '_')}.json`);
|
|
162
|
-
try {
|
|
163
|
-
fs.mkdirSync(CONV_DIR, { recursive: true });
|
|
164
|
-
if (fs.existsSync(file)) {
|
|
165
|
-
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
166
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
167
|
-
}
|
|
168
|
-
} catch (e) {
|
|
169
|
-
// Silently fail
|
|
170
|
-
}
|
|
171
|
-
return [];
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Save conversation to disk
|
|
176
|
-
*/
|
|
177
|
-
function saveConversation(convId, messages) {
|
|
178
|
-
const file = path.join(CONV_DIR, `${convId.replace(/[^a-z0-9]/gi, '_')}.json`);
|
|
179
|
-
try {
|
|
180
|
-
fs.mkdirSync(CONV_DIR, { recursive: true });
|
|
181
|
-
// Keep only last 10 messages
|
|
182
|
-
fs.writeFileSync(file, JSON.stringify(messages.slice(-(TB.load().conversationOnDisk)), null, 2));
|
|
183
|
-
} catch (e) {
|
|
184
|
-
// Silently fail
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Start MCP servers from config
|
|
190
|
-
*/
|
|
191
|
-
async function startMcpServers() {
|
|
192
|
-
const config = getConfig();
|
|
193
|
-
|
|
194
|
-
// Skip if MCP is disabled
|
|
195
|
-
if (config.mcpEnabled === false) {
|
|
196
|
-
debugLog('[MCP] MCP is disabled in config, skipping server startup');
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const servers = config.mcpServers || {};
|
|
201
|
-
|
|
202
|
-
for (const [name, server] of Object.entries(servers)) {
|
|
203
|
-
// Skip disabled servers
|
|
204
|
-
if (server.disabled) {
|
|
205
|
-
debugLog(`[MCP] Skipping disabled server: ${name}`);
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Skip if already started
|
|
210
|
-
if (mcpClients[name]) {
|
|
211
|
-
debugLog(`[MCP] Server already running: ${name}`);
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
try {
|
|
216
|
-
debugLog(`[MCP] Starting server: ${name}`);
|
|
217
|
-
|
|
218
|
-
const client = new MCPClient(server.command, server.args, server.env || {});
|
|
219
|
-
await client.start();
|
|
220
|
-
|
|
221
|
-
const tools = await client.listTools();
|
|
222
|
-
debugLog(`[MCP] Server ${name} loaded ${tools.length} tools`);
|
|
223
|
-
|
|
224
|
-
mcpClients[name] = { client, tools };
|
|
225
|
-
|
|
226
|
-
} catch (err) {
|
|
227
|
-
debugLog(`[MCP] Failed to start server ${name}: ${err.message}`);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Stop all MCP servers
|
|
234
|
-
*/
|
|
235
|
-
function stopMcpServers() {
|
|
236
|
-
for (const [name, { client }] of Object.entries(mcpClients)) {
|
|
237
|
-
try {
|
|
238
|
-
debugLog(`[MCP] Stopping server: ${name}`);
|
|
239
|
-
client.stop();
|
|
240
|
-
} catch (err) {
|
|
241
|
-
debugLog(`[MCP] Failed to stop server ${name}: ${err.message}`);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Clear clients
|
|
246
|
-
Object.keys(mcpClients).forEach(key => delete mcpClients[key]);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Get all MCP tools (combined from all servers)
|
|
251
|
-
*/
|
|
252
|
-
function getMcpTools() {
|
|
253
|
-
const allTools = [];
|
|
254
|
-
|
|
255
|
-
for (const [serverName, { tools }] of Object.entries(mcpClients)) {
|
|
256
|
-
for (const tool of tools) {
|
|
257
|
-
allTools.push({
|
|
258
|
-
...tool,
|
|
259
|
-
_mcpServer: serverName, // Track which server this tool belongs to
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return allTools;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Groq-incompatible MCP tools (strict validation issues)
|
|
268
|
-
const BLOCKED_MCP_TOOLS = ['search_issues', 'search_repositories'];
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Get MCP tools filtered for AI consumption
|
|
272
|
-
* Removes tools that are incompatible with Groq's strict validation
|
|
273
|
-
*/
|
|
274
|
-
function getMcpToolsForAI() {
|
|
275
|
-
const tools = getMcpTools();
|
|
276
|
-
return tools.filter(t => !BLOCKED_MCP_TOOLS.includes(t.name));
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Normalize MCP tool schema for AI consumption
|
|
281
|
-
* Adds hints to number/integer parameters to prevent string conversion
|
|
282
|
-
*/
|
|
283
|
-
function normalizeMcpToolSchema(tool) {
|
|
284
|
-
const schema = tool.inputSchema || tool.input_schema || {};
|
|
285
|
-
|
|
286
|
-
// Ensure properties exist
|
|
287
|
-
if (!schema.properties) return tool;
|
|
288
|
-
|
|
289
|
-
// Clone schema to avoid mutating original
|
|
290
|
-
const normalizedSchema = JSON.parse(JSON.stringify(schema));
|
|
291
|
-
|
|
292
|
-
// Groq sometimes sends strings for number params
|
|
293
|
-
// Add coercion hint to description
|
|
294
|
-
for (const [key, prop] of Object.entries(normalizedSchema.properties)) {
|
|
295
|
-
if (prop.type === 'number' || prop.type === 'integer') {
|
|
296
|
-
prop.description = (prop.description || '') + ' (must be a number, not a string)';
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
return { ...tool, inputSchema: normalizedSchema };
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Minimize MCP tool schema to reduce token usage
|
|
305
|
-
* Truncates descriptions and removes unnecessary fields
|
|
306
|
-
*/
|
|
307
|
-
function minimizeMcpTool(tool) {
|
|
308
|
-
return {
|
|
309
|
-
name: tool.name,
|
|
310
|
-
description: TB.capMcpDesc(tool.description),
|
|
311
|
-
inputSchema: {
|
|
312
|
-
type: tool.inputSchema?.type || 'object',
|
|
313
|
-
properties: Object.fromEntries(
|
|
314
|
-
Object.entries(tool.inputSchema?.properties || {}).map(([k, v]) => [
|
|
315
|
-
k,
|
|
316
|
-
{
|
|
317
|
-
type: v.type,
|
|
318
|
-
...(v.enum ? { enum: v.enum } : {}) // Include enum only if exists
|
|
319
|
-
}
|
|
320
|
-
])
|
|
321
|
-
),
|
|
322
|
-
required: tool.inputSchema?.required || []
|
|
323
|
-
}
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Coerce MCP tool parameters to match schema types
|
|
329
|
-
*/
|
|
330
|
-
function coerceMcpParams(tool, params) {
|
|
331
|
-
// GitHub MCP uses inputSchema, others may use input_schema
|
|
332
|
-
// Try all possible schema locations
|
|
333
|
-
const schema = tool.inputSchema?.properties ||
|
|
334
|
-
tool.input_schema?.properties ||
|
|
335
|
-
tool.parameters?.properties || // fallback
|
|
336
|
-
{};
|
|
337
|
-
|
|
338
|
-
const coerced = { ...params };
|
|
339
|
-
|
|
340
|
-
for (const [key, def] of Object.entries(schema)) {
|
|
341
|
-
if (coerced[key] === undefined || coerced[key] === null) continue;
|
|
342
|
-
|
|
343
|
-
// Coerce number or integer
|
|
344
|
-
if ((def.type === 'number' || def.type === 'integer') && typeof coerced[key] === 'string') {
|
|
345
|
-
const num = Number(coerced[key]);
|
|
346
|
-
if (!isNaN(num)) {
|
|
347
|
-
coerced[key] = num;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Coerce boolean
|
|
352
|
-
if (def.type === 'boolean' && typeof coerced[key] === 'string') {
|
|
353
|
-
coerced[key] = coerced[key] === 'true' || coerced[key] === '1';
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
return coerced;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Execute MCP tool call
|
|
362
|
-
*/
|
|
363
|
-
async function executeMcpTool(toolName, toolArgs) {
|
|
364
|
-
// Find which server has this tool
|
|
365
|
-
for (const [serverName, { client, tools }] of Object.entries(mcpClients)) {
|
|
366
|
-
const tool = tools.find(t => t.name === toolName);
|
|
367
|
-
|
|
368
|
-
if (tool) {
|
|
369
|
-
debugLog(`[MCP] Calling tool ${toolName} on server ${serverName}`);
|
|
370
|
-
|
|
371
|
-
try {
|
|
372
|
-
// Coerce parameters to match schema types
|
|
373
|
-
const coercedArgs = coerceMcpParams(tool, toolArgs);
|
|
374
|
-
|
|
375
|
-
const result = await client.callTool(toolName, coercedArgs);
|
|
376
|
-
|
|
377
|
-
// MCP returns { content: [{ type: 'text', text: '...' }] }
|
|
378
|
-
if (result.content && result.content.length > 0) {
|
|
379
|
-
// Extract all text content and join with newlines
|
|
380
|
-
const textContents = result.content
|
|
381
|
-
.filter(c => c.type === 'text')
|
|
382
|
-
.map(c => c.text);
|
|
383
|
-
|
|
384
|
-
if (textContents.length > 0) {
|
|
385
|
-
let output = textContents.join('\n');
|
|
386
|
-
|
|
387
|
-
// Truncate MCP result
|
|
388
|
-
const maxChars = TB.load().toolMaxChars;
|
|
389
|
-
if (output.length > maxChars) {
|
|
390
|
-
output = output.slice(0, maxChars) + '... (truncated)';
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
return {
|
|
394
|
-
success: true,
|
|
395
|
-
output: output
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// Fallback: return entire result as JSON
|
|
401
|
-
let fallbackOutput = JSON.stringify(result, null, 2);
|
|
402
|
-
|
|
403
|
-
// Truncate fallback output too
|
|
404
|
-
const maxChars = TB.load().toolMaxChars;
|
|
405
|
-
if (fallbackOutput.length > maxChars) {
|
|
406
|
-
fallbackOutput = fallbackOutput.slice(0, maxChars) + '... (truncated)';
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
return {
|
|
410
|
-
success: true,
|
|
411
|
-
output: fallbackOutput
|
|
412
|
-
};
|
|
413
|
-
|
|
414
|
-
} catch (err) {
|
|
415
|
-
return {
|
|
416
|
-
success: false,
|
|
417
|
-
error: `MCP tool error: ${err.message}`
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
return {
|
|
424
|
-
success: false,
|
|
425
|
-
error: `MCP tool not found: ${toolName}`
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
/**
|
|
430
|
-
* Encode tool result for safe transmission
|
|
431
|
-
* Works for both MCP and local tools
|
|
432
|
-
*/
|
|
433
|
-
function encodeToolResult(toolResult) {
|
|
434
|
-
let content;
|
|
435
|
-
|
|
436
|
-
// Handle different result formats
|
|
437
|
-
if (typeof toolResult === 'string') {
|
|
438
|
-
content = toolResult;
|
|
439
|
-
} else if (toolResult.output) {
|
|
440
|
-
content = toolResult.output;
|
|
441
|
-
} else if (toolResult.success !== undefined) {
|
|
442
|
-
// Handle { success: true/false, output/error: ... } format
|
|
443
|
-
content = toolResult.success ? (toolResult.output || JSON.stringify(toolResult)) : (toolResult.error || 'Unknown error');
|
|
444
|
-
} else {
|
|
445
|
-
content = JSON.stringify(toolResult);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// Base64 encode
|
|
449
|
-
const encoded = Buffer.from(content).toString('base64');
|
|
450
|
-
return `[BASE64_ENCODED_RESULT]: ${encoded}`;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/**
|
|
454
|
-
* Check if debug mode is enabled
|
|
455
|
-
*/
|
|
456
|
-
function isDebugEnabled() {
|
|
457
|
-
const config = getConfig();
|
|
458
|
-
return config.debug === true || config.debug === 'true';
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* Debug log (only if debug mode enabled)
|
|
463
|
-
*/
|
|
464
|
-
function debugLog(...args) {
|
|
465
|
-
if (isDebugEnabled()) {
|
|
466
|
-
console.log(...args);
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
/**
|
|
471
|
-
* Get provider configuration from config
|
|
472
|
-
*/
|
|
473
|
-
function getProviderConfig() {
|
|
474
|
-
const config = getConfig();
|
|
475
|
-
|
|
476
|
-
// Universal provider config (v2.1.0+)
|
|
477
|
-
if (config.providerUrl && config.providerApiKey) {
|
|
478
|
-
return {
|
|
479
|
-
url: config.providerUrl,
|
|
480
|
-
apiKey: config.providerApiKey,
|
|
481
|
-
model: config.providerModel || 'llama-3.3-70b-versatile',
|
|
482
|
-
isAnthropic: config.providerUrl.includes('anthropic.com')
|
|
483
|
-
};
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// Legacy Groq config (v2.0.x)
|
|
487
|
-
if (config.groqApiKey) {
|
|
488
|
-
return {
|
|
489
|
-
url: 'https://api.groq.com/openai/v1',
|
|
490
|
-
apiKey: config.groqApiKey,
|
|
491
|
-
model: config.groqModel || 'llama-3.3-70b-versatile',
|
|
492
|
-
isAnthropic: false
|
|
493
|
-
};
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
return null;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
/**
|
|
500
|
-
* Format tool definitions for OpenAI-compatible APIs
|
|
501
|
-
*/
|
|
502
|
-
function formatToolsForOpenAI() {
|
|
503
|
-
const config = getConfig();
|
|
504
|
-
const localTools = getToolDefinitions();
|
|
505
|
-
|
|
506
|
-
// Only add MCP tools if enabled
|
|
507
|
-
let allTools = [...localTools];
|
|
508
|
-
if (config.mcpEnabled !== false) {
|
|
509
|
-
const mcpTools = getMcpToolsForAI().map(minimizeMcpTool);
|
|
510
|
-
const normalizedMcpTools = mcpTools.map(tool => normalizeMcpToolSchema(tool));
|
|
511
|
-
allTools = [...allTools, ...normalizedMcpTools];
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
return allTools.map(tool => ({
|
|
515
|
-
type: 'function',
|
|
516
|
-
function: {
|
|
517
|
-
name: tool.name,
|
|
518
|
-
description: tool.description,
|
|
519
|
-
parameters: tool.inputSchema || tool.input_schema || { type: 'object', properties: {} }
|
|
520
|
-
}
|
|
521
|
-
}));
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
/**
|
|
525
|
-
* Format tool definitions for Anthropic API
|
|
526
|
-
*/
|
|
527
|
-
function formatToolsForAnthropic() {
|
|
528
|
-
const config = getConfig();
|
|
529
|
-
const localTools = getToolDefinitions();
|
|
530
|
-
|
|
531
|
-
// Only add MCP tools if enabled
|
|
532
|
-
let allTools = [...localTools];
|
|
533
|
-
if (config.mcpEnabled !== false) {
|
|
534
|
-
const mcpTools = getMcpToolsForAI().map(minimizeMcpTool);
|
|
535
|
-
const normalizedMcpTools = mcpTools.map(tool => normalizeMcpToolSchema(tool));
|
|
536
|
-
allTools = [...allTools, ...normalizedMcpTools];
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
return allTools.map(tool => ({
|
|
540
|
-
name: tool.name,
|
|
541
|
-
description: tool.description,
|
|
542
|
-
input_schema: tool.inputSchema || tool.input_schema || { type: 'object', properties: {} }
|
|
543
|
-
}));
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
/**
|
|
547
|
-
* Send message to OpenAI-compatible provider (Groq, OpenAI, Together, etc.)
|
|
548
|
-
*/
|
|
549
|
-
async function sendMessageOpenAICompatible(providerConfig, messages, tools) {
|
|
550
|
-
const baseUrl = providerConfig.url.replace(/\/+$/, '');
|
|
551
|
-
//
|
|
552
|
-
const endpoint =
|
|
553
|
-
|
|
554
|
-
:
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
})
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
const
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
];
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
//
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
//
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
result.error =
|
|
954
|
-
return result;
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
try {
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
}
|
|
1019
|
-
return
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
const
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
if (
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
const
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
if (
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
tool_calls
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
const
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
}
|
|
1
|
+
// NatureCo CLI v2.10.1 - Universal LLM Provider Support + MCP Integration
|
|
2
|
+
// Supports: OpenAI, Groq, Together, Fireworks, Perplexity, Mistral, DeepSeek, OpenRouter, Ollama, LM Studio, Anthropic
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const { getConfig } = require('./config');
|
|
9
|
+
const { getToolDefinitions, executeToolCalls } = require('./tool-runner');
|
|
10
|
+
const { MCPClient } = require('./mcp-client');
|
|
11
|
+
const TB = require('./token-budget');
|
|
12
|
+
const { accumulateToolCallDeltas } = require('./streaming-tools');
|
|
13
|
+
const { ToolGuardrails } = require('./tool-guardrails');
|
|
14
|
+
const guardrails = new ToolGuardrails();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* v5.5.0: Provider-specific format detection
|
|
18
|
+
* Groq, OpenAI, Anthropic, Mistral, DeepSeek, OpenRouter, Ollama, MiniMax
|
|
19
|
+
*
|
|
20
|
+
* Canonical implementation lives in src/utils/provider-detect.js;
|
|
21
|
+
* re-exported here so the historical `detectProvider` reference inside
|
|
22
|
+
* api.js continues to work without touching every call site.
|
|
23
|
+
*/
|
|
24
|
+
const { detectProvider, isMiniMax, isGemini, buildChatEndpoint } = require('./provider-detect');
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* v5.5.0: Tool definitions'ı provider'a göre normalize et
|
|
28
|
+
* - OpenAI/Groq/Mistral/DeepSeek/OpenRouter: tool_choice, function calling OK
|
|
29
|
+
* - Anthropic: tools, system ayrı, content array
|
|
30
|
+
* - Ollama: tool support sınırlı, genelde yok
|
|
31
|
+
* - Perplexity: tool support yok
|
|
32
|
+
*/
|
|
33
|
+
function normalizeToolsForProvider(tools, provider) {
|
|
34
|
+
if (!tools || tools.length === 0) return tools;
|
|
35
|
+
if (provider === 'ollama' || provider === 'perplexity') {
|
|
36
|
+
// Bu providerlar tool support etmiyor - bos dondur
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
if (provider === 'anthropic') {
|
|
40
|
+
// Anthropic tools format: { name, description, input_schema }
|
|
41
|
+
return tools.map(t => ({
|
|
42
|
+
name: t.function.name,
|
|
43
|
+
description: t.function.description,
|
|
44
|
+
input_schema: t.function.parameters || { type: 'object', properties: {} }
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
// OpenAI-compatible (Groq, Mistral, DeepSeek, OpenRouter, MiniMax, Together, Fireworks)
|
|
48
|
+
return tools;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* v5.5.0: Tool call'ları provider'a göre parse et
|
|
53
|
+
* OpenAI: tool_calls[].function.arguments (string)
|
|
54
|
+
* Anthropic: content[].type=tool_use, input (object)
|
|
55
|
+
* Ollama: genelde yok
|
|
56
|
+
*/
|
|
57
|
+
function parseToolCallsFromResponse(message, provider) {
|
|
58
|
+
if (provider === 'anthropic') {
|
|
59
|
+
// Anthropic: content array içinde tool_use blokları var
|
|
60
|
+
if (Array.isArray(message.content)) {
|
|
61
|
+
const toolUses = message.content.filter(c => c.type === 'tool_use');
|
|
62
|
+
return toolUses.map(tu => ({
|
|
63
|
+
id: tu.id,
|
|
64
|
+
type: 'function',
|
|
65
|
+
function: {
|
|
66
|
+
name: tu.name,
|
|
67
|
+
arguments: JSON.stringify(tu.input || {})
|
|
68
|
+
}
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
// OpenAI-compatible
|
|
74
|
+
return message.tool_calls || [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Anthropic Messages API requires `system` to be either a non-empty string
|
|
79
|
+
* or omitted entirely. Sending `undefined` (which JSON.stringify drops to
|
|
80
|
+
* silent absence) leaves the model unanchored; sending `''` returns 400
|
|
81
|
+
* "system: cannot be empty" on recent API revisions. Always pass a
|
|
82
|
+
* meaningful default when no system message is present.
|
|
83
|
+
*/
|
|
84
|
+
const DEFAULT_ANTHROPIC_SYSTEM =
|
|
85
|
+
'You are a helpful AI assistant running inside the natureco CLI.';
|
|
86
|
+
|
|
87
|
+
function extractSystemForAnthropic(messages) {
|
|
88
|
+
const systemMsg = messages.find(m => m.role === 'system');
|
|
89
|
+
if (!systemMsg) return DEFAULT_ANTHROPIC_SYSTEM;
|
|
90
|
+
// content may be a string or an array of content blocks; both round-trip.
|
|
91
|
+
if (typeof systemMsg.content === 'string') {
|
|
92
|
+
return systemMsg.content.trim() || DEFAULT_ANTHROPIC_SYSTEM;
|
|
93
|
+
}
|
|
94
|
+
if (Array.isArray(systemMsg.content) && systemMsg.content.length > 0) {
|
|
95
|
+
return systemMsg.content;
|
|
96
|
+
}
|
|
97
|
+
return DEFAULT_ANTHROPIC_SYSTEM;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* v5.5.0: System mesajı provider'a göre ayarla
|
|
102
|
+
* - OpenAI: messages[].role=system
|
|
103
|
+
* - Anthropic: ayrı 'system' field
|
|
104
|
+
*/
|
|
105
|
+
function buildRequestBody(messages, model, options, provider) {
|
|
106
|
+
if (provider === 'anthropic') {
|
|
107
|
+
const userMsgs = messages.filter(m => m.role !== 'system');
|
|
108
|
+
return {
|
|
109
|
+
model,
|
|
110
|
+
messages: userMsgs.map(m => ({
|
|
111
|
+
role: m.role,
|
|
112
|
+
content: m.content
|
|
113
|
+
})),
|
|
114
|
+
system: extractSystemForAnthropic(messages),
|
|
115
|
+
max_tokens: options.max_tokens || 4096,
|
|
116
|
+
temperature: options.temperature || 0.7,
|
|
117
|
+
...(options.tools && options.tools.length > 0 ? { tools: options.tools } : {})
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// OpenAI-compatible
|
|
121
|
+
return {
|
|
122
|
+
model,
|
|
123
|
+
messages,
|
|
124
|
+
max_tokens: options.max_tokens || 4096,
|
|
125
|
+
temperature: options.temperature || 0.7,
|
|
126
|
+
...(options.tools && options.tools.length > 0 ? { tools: options.tools, tool_choice: 'auto' } : {})
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
// Persistent conversation directory
|
|
132
|
+
const CONV_DIR = path.join(os.homedir(), '.natureco', 'conversations');
|
|
133
|
+
|
|
134
|
+
// Conversation history for multi-turn chat (deprecated - now using disk storage)
|
|
135
|
+
const conversationHistory = new Map();
|
|
136
|
+
|
|
137
|
+
// MCP clients (server name -> { client, tools })
|
|
138
|
+
const mcpClients = {};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Generate default conversation ID based on provider config
|
|
142
|
+
*/
|
|
143
|
+
function generateDefaultConvId() {
|
|
144
|
+
const config = getConfig();
|
|
145
|
+
|
|
146
|
+
// Use provider URL + model as base for consistent ID
|
|
147
|
+
const providerUrl = config.providerUrl || 'default';
|
|
148
|
+
const model = config.providerModel || 'default';
|
|
149
|
+
|
|
150
|
+
// Create simple hash-like ID from provider + model
|
|
151
|
+
const base = `${providerUrl}_${model}`.replace(/[^a-z0-9]/gi, '_').toLowerCase();
|
|
152
|
+
|
|
153
|
+
// Return consistent ID (e.g., "groq_llama_3_1_8b_instant")
|
|
154
|
+
return base.slice(0, 50); // Limit length
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Load conversation from disk
|
|
159
|
+
*/
|
|
160
|
+
function loadConversation(convId) {
|
|
161
|
+
const file = path.join(CONV_DIR, `${convId.replace(/[^a-z0-9]/gi, '_')}.json`);
|
|
162
|
+
try {
|
|
163
|
+
fs.mkdirSync(CONV_DIR, { recursive: true });
|
|
164
|
+
if (fs.existsSync(file)) {
|
|
165
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
166
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
167
|
+
}
|
|
168
|
+
} catch (e) {
|
|
169
|
+
// Silently fail
|
|
170
|
+
}
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Save conversation to disk
|
|
176
|
+
*/
|
|
177
|
+
function saveConversation(convId, messages) {
|
|
178
|
+
const file = path.join(CONV_DIR, `${convId.replace(/[^a-z0-9]/gi, '_')}.json`);
|
|
179
|
+
try {
|
|
180
|
+
fs.mkdirSync(CONV_DIR, { recursive: true });
|
|
181
|
+
// Keep only last 10 messages
|
|
182
|
+
fs.writeFileSync(file, JSON.stringify(messages.slice(-(TB.load().conversationOnDisk)), null, 2));
|
|
183
|
+
} catch (e) {
|
|
184
|
+
// Silently fail
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Start MCP servers from config
|
|
190
|
+
*/
|
|
191
|
+
async function startMcpServers() {
|
|
192
|
+
const config = getConfig();
|
|
193
|
+
|
|
194
|
+
// Skip if MCP is disabled
|
|
195
|
+
if (config.mcpEnabled === false) {
|
|
196
|
+
debugLog('[MCP] MCP is disabled in config, skipping server startup');
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const servers = config.mcpServers || {};
|
|
201
|
+
|
|
202
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
203
|
+
// Skip disabled servers
|
|
204
|
+
if (server.disabled) {
|
|
205
|
+
debugLog(`[MCP] Skipping disabled server: ${name}`);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Skip if already started
|
|
210
|
+
if (mcpClients[name]) {
|
|
211
|
+
debugLog(`[MCP] Server already running: ${name}`);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
debugLog(`[MCP] Starting server: ${name}`);
|
|
217
|
+
|
|
218
|
+
const client = new MCPClient(server.command, server.args, server.env || {});
|
|
219
|
+
await client.start();
|
|
220
|
+
|
|
221
|
+
const tools = await client.listTools();
|
|
222
|
+
debugLog(`[MCP] Server ${name} loaded ${tools.length} tools`);
|
|
223
|
+
|
|
224
|
+
mcpClients[name] = { client, tools };
|
|
225
|
+
|
|
226
|
+
} catch (err) {
|
|
227
|
+
debugLog(`[MCP] Failed to start server ${name}: ${err.message}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Stop all MCP servers
|
|
234
|
+
*/
|
|
235
|
+
function stopMcpServers() {
|
|
236
|
+
for (const [name, { client }] of Object.entries(mcpClients)) {
|
|
237
|
+
try {
|
|
238
|
+
debugLog(`[MCP] Stopping server: ${name}`);
|
|
239
|
+
client.stop();
|
|
240
|
+
} catch (err) {
|
|
241
|
+
debugLog(`[MCP] Failed to stop server ${name}: ${err.message}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Clear clients
|
|
246
|
+
Object.keys(mcpClients).forEach(key => delete mcpClients[key]);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Get all MCP tools (combined from all servers)
|
|
251
|
+
*/
|
|
252
|
+
function getMcpTools() {
|
|
253
|
+
const allTools = [];
|
|
254
|
+
|
|
255
|
+
for (const [serverName, { tools }] of Object.entries(mcpClients)) {
|
|
256
|
+
for (const tool of tools) {
|
|
257
|
+
allTools.push({
|
|
258
|
+
...tool,
|
|
259
|
+
_mcpServer: serverName, // Track which server this tool belongs to
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return allTools;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Groq-incompatible MCP tools (strict validation issues)
|
|
268
|
+
const BLOCKED_MCP_TOOLS = ['search_issues', 'search_repositories'];
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Get MCP tools filtered for AI consumption
|
|
272
|
+
* Removes tools that are incompatible with Groq's strict validation
|
|
273
|
+
*/
|
|
274
|
+
function getMcpToolsForAI() {
|
|
275
|
+
const tools = getMcpTools();
|
|
276
|
+
return tools.filter(t => !BLOCKED_MCP_TOOLS.includes(t.name));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Normalize MCP tool schema for AI consumption
|
|
281
|
+
* Adds hints to number/integer parameters to prevent string conversion
|
|
282
|
+
*/
|
|
283
|
+
function normalizeMcpToolSchema(tool) {
|
|
284
|
+
const schema = tool.inputSchema || tool.input_schema || {};
|
|
285
|
+
|
|
286
|
+
// Ensure properties exist
|
|
287
|
+
if (!schema.properties) return tool;
|
|
288
|
+
|
|
289
|
+
// Clone schema to avoid mutating original
|
|
290
|
+
const normalizedSchema = JSON.parse(JSON.stringify(schema));
|
|
291
|
+
|
|
292
|
+
// Groq sometimes sends strings for number params
|
|
293
|
+
// Add coercion hint to description
|
|
294
|
+
for (const [key, prop] of Object.entries(normalizedSchema.properties)) {
|
|
295
|
+
if (prop.type === 'number' || prop.type === 'integer') {
|
|
296
|
+
prop.description = (prop.description || '') + ' (must be a number, not a string)';
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return { ...tool, inputSchema: normalizedSchema };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Minimize MCP tool schema to reduce token usage
|
|
305
|
+
* Truncates descriptions and removes unnecessary fields
|
|
306
|
+
*/
|
|
307
|
+
function minimizeMcpTool(tool) {
|
|
308
|
+
return {
|
|
309
|
+
name: tool.name,
|
|
310
|
+
description: TB.capMcpDesc(tool.description),
|
|
311
|
+
inputSchema: {
|
|
312
|
+
type: tool.inputSchema?.type || 'object',
|
|
313
|
+
properties: Object.fromEntries(
|
|
314
|
+
Object.entries(tool.inputSchema?.properties || {}).map(([k, v]) => [
|
|
315
|
+
k,
|
|
316
|
+
{
|
|
317
|
+
type: v.type,
|
|
318
|
+
...(v.enum ? { enum: v.enum } : {}) // Include enum only if exists
|
|
319
|
+
}
|
|
320
|
+
])
|
|
321
|
+
),
|
|
322
|
+
required: tool.inputSchema?.required || []
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Coerce MCP tool parameters to match schema types
|
|
329
|
+
*/
|
|
330
|
+
function coerceMcpParams(tool, params) {
|
|
331
|
+
// GitHub MCP uses inputSchema, others may use input_schema
|
|
332
|
+
// Try all possible schema locations
|
|
333
|
+
const schema = tool.inputSchema?.properties ||
|
|
334
|
+
tool.input_schema?.properties ||
|
|
335
|
+
tool.parameters?.properties || // fallback
|
|
336
|
+
{};
|
|
337
|
+
|
|
338
|
+
const coerced = { ...params };
|
|
339
|
+
|
|
340
|
+
for (const [key, def] of Object.entries(schema)) {
|
|
341
|
+
if (coerced[key] === undefined || coerced[key] === null) continue;
|
|
342
|
+
|
|
343
|
+
// Coerce number or integer
|
|
344
|
+
if ((def.type === 'number' || def.type === 'integer') && typeof coerced[key] === 'string') {
|
|
345
|
+
const num = Number(coerced[key]);
|
|
346
|
+
if (!isNaN(num)) {
|
|
347
|
+
coerced[key] = num;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Coerce boolean
|
|
352
|
+
if (def.type === 'boolean' && typeof coerced[key] === 'string') {
|
|
353
|
+
coerced[key] = coerced[key] === 'true' || coerced[key] === '1';
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return coerced;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Execute MCP tool call
|
|
362
|
+
*/
|
|
363
|
+
async function executeMcpTool(toolName, toolArgs) {
|
|
364
|
+
// Find which server has this tool
|
|
365
|
+
for (const [serverName, { client, tools }] of Object.entries(mcpClients)) {
|
|
366
|
+
const tool = tools.find(t => t.name === toolName);
|
|
367
|
+
|
|
368
|
+
if (tool) {
|
|
369
|
+
debugLog(`[MCP] Calling tool ${toolName} on server ${serverName}`);
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
// Coerce parameters to match schema types
|
|
373
|
+
const coercedArgs = coerceMcpParams(tool, toolArgs);
|
|
374
|
+
|
|
375
|
+
const result = await client.callTool(toolName, coercedArgs);
|
|
376
|
+
|
|
377
|
+
// MCP returns { content: [{ type: 'text', text: '...' }] }
|
|
378
|
+
if (result.content && result.content.length > 0) {
|
|
379
|
+
// Extract all text content and join with newlines
|
|
380
|
+
const textContents = result.content
|
|
381
|
+
.filter(c => c.type === 'text')
|
|
382
|
+
.map(c => c.text);
|
|
383
|
+
|
|
384
|
+
if (textContents.length > 0) {
|
|
385
|
+
let output = textContents.join('\n');
|
|
386
|
+
|
|
387
|
+
// Truncate MCP result
|
|
388
|
+
const maxChars = TB.load().toolMaxChars;
|
|
389
|
+
if (output.length > maxChars) {
|
|
390
|
+
output = output.slice(0, maxChars) + '... (truncated)';
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
success: true,
|
|
395
|
+
output: output
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Fallback: return entire result as JSON
|
|
401
|
+
let fallbackOutput = JSON.stringify(result, null, 2);
|
|
402
|
+
|
|
403
|
+
// Truncate fallback output too
|
|
404
|
+
const maxChars = TB.load().toolMaxChars;
|
|
405
|
+
if (fallbackOutput.length > maxChars) {
|
|
406
|
+
fallbackOutput = fallbackOutput.slice(0, maxChars) + '... (truncated)';
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
success: true,
|
|
411
|
+
output: fallbackOutput
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
} catch (err) {
|
|
415
|
+
return {
|
|
416
|
+
success: false,
|
|
417
|
+
error: `MCP tool error: ${err.message}`
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
success: false,
|
|
425
|
+
error: `MCP tool not found: ${toolName}`
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Encode tool result for safe transmission
|
|
431
|
+
* Works for both MCP and local tools
|
|
432
|
+
*/
|
|
433
|
+
function encodeToolResult(toolResult) {
|
|
434
|
+
let content;
|
|
435
|
+
|
|
436
|
+
// Handle different result formats
|
|
437
|
+
if (typeof toolResult === 'string') {
|
|
438
|
+
content = toolResult;
|
|
439
|
+
} else if (toolResult.output) {
|
|
440
|
+
content = toolResult.output;
|
|
441
|
+
} else if (toolResult.success !== undefined) {
|
|
442
|
+
// Handle { success: true/false, output/error: ... } format
|
|
443
|
+
content = toolResult.success ? (toolResult.output || JSON.stringify(toolResult)) : (toolResult.error || 'Unknown error');
|
|
444
|
+
} else {
|
|
445
|
+
content = JSON.stringify(toolResult);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Base64 encode
|
|
449
|
+
const encoded = Buffer.from(content).toString('base64');
|
|
450
|
+
return `[BASE64_ENCODED_RESULT]: ${encoded}`;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Check if debug mode is enabled
|
|
455
|
+
*/
|
|
456
|
+
function isDebugEnabled() {
|
|
457
|
+
const config = getConfig();
|
|
458
|
+
return config.debug === true || config.debug === 'true';
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Debug log (only if debug mode enabled)
|
|
463
|
+
*/
|
|
464
|
+
function debugLog(...args) {
|
|
465
|
+
if (isDebugEnabled()) {
|
|
466
|
+
console.log(...args);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Get provider configuration from config
|
|
472
|
+
*/
|
|
473
|
+
function getProviderConfig() {
|
|
474
|
+
const config = getConfig();
|
|
475
|
+
|
|
476
|
+
// Universal provider config (v2.1.0+)
|
|
477
|
+
if (config.providerUrl && config.providerApiKey) {
|
|
478
|
+
return {
|
|
479
|
+
url: config.providerUrl,
|
|
480
|
+
apiKey: config.providerApiKey,
|
|
481
|
+
model: config.providerModel || 'llama-3.3-70b-versatile',
|
|
482
|
+
isAnthropic: config.providerUrl.includes('anthropic.com')
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Legacy Groq config (v2.0.x)
|
|
487
|
+
if (config.groqApiKey) {
|
|
488
|
+
return {
|
|
489
|
+
url: 'https://api.groq.com/openai/v1',
|
|
490
|
+
apiKey: config.groqApiKey,
|
|
491
|
+
model: config.groqModel || 'llama-3.3-70b-versatile',
|
|
492
|
+
isAnthropic: false
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Format tool definitions for OpenAI-compatible APIs
|
|
501
|
+
*/
|
|
502
|
+
function formatToolsForOpenAI() {
|
|
503
|
+
const config = getConfig();
|
|
504
|
+
const localTools = getToolDefinitions();
|
|
505
|
+
|
|
506
|
+
// Only add MCP tools if enabled
|
|
507
|
+
let allTools = [...localTools];
|
|
508
|
+
if (config.mcpEnabled !== false) {
|
|
509
|
+
const mcpTools = getMcpToolsForAI().map(minimizeMcpTool);
|
|
510
|
+
const normalizedMcpTools = mcpTools.map(tool => normalizeMcpToolSchema(tool));
|
|
511
|
+
allTools = [...allTools, ...normalizedMcpTools];
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return allTools.map(tool => ({
|
|
515
|
+
type: 'function',
|
|
516
|
+
function: {
|
|
517
|
+
name: tool.name,
|
|
518
|
+
description: tool.description,
|
|
519
|
+
parameters: tool.inputSchema || tool.input_schema || { type: 'object', properties: {} }
|
|
520
|
+
}
|
|
521
|
+
}));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Format tool definitions for Anthropic API
|
|
526
|
+
*/
|
|
527
|
+
function formatToolsForAnthropic() {
|
|
528
|
+
const config = getConfig();
|
|
529
|
+
const localTools = getToolDefinitions();
|
|
530
|
+
|
|
531
|
+
// Only add MCP tools if enabled
|
|
532
|
+
let allTools = [...localTools];
|
|
533
|
+
if (config.mcpEnabled !== false) {
|
|
534
|
+
const mcpTools = getMcpToolsForAI().map(minimizeMcpTool);
|
|
535
|
+
const normalizedMcpTools = mcpTools.map(tool => normalizeMcpToolSchema(tool));
|
|
536
|
+
allTools = [...allTools, ...normalizedMcpTools];
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return allTools.map(tool => ({
|
|
540
|
+
name: tool.name,
|
|
541
|
+
description: tool.description,
|
|
542
|
+
input_schema: tool.inputSchema || tool.input_schema || { type: 'object', properties: {} }
|
|
543
|
+
}));
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Send message to OpenAI-compatible provider (Groq, OpenAI, Together, etc.)
|
|
548
|
+
*/
|
|
549
|
+
async function sendMessageOpenAICompatible(providerConfig, messages, tools) {
|
|
550
|
+
const baseUrl = providerConfig.url.replace(/\/+$/, '');
|
|
551
|
+
// Tek doğruluk kaynağı: provider-detect.buildChatEndpoint (MiniMax /v1 toleransı dahil)
|
|
552
|
+
const endpoint = buildChatEndpoint(baseUrl);
|
|
553
|
+
const requestBody = {
|
|
554
|
+
model: providerConfig.model,
|
|
555
|
+
messages: messages,
|
|
556
|
+
temperature: 0.7,
|
|
557
|
+
max_tokens: 2048,
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
// NatureCo için tool calling desteklenmiyor
|
|
561
|
+
if (!providerConfig.url.includes('api.natureco.me')) {
|
|
562
|
+
if (tools && tools.length > 0) {
|
|
563
|
+
requestBody.tools = tools;
|
|
564
|
+
requestBody.tool_choice = 'auto';
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const response = await fetch(endpoint, {
|
|
569
|
+
method: 'POST',
|
|
570
|
+
headers: {
|
|
571
|
+
'Authorization': `Bearer ${providerConfig.apiKey}`,
|
|
572
|
+
'Content-Type': 'application/json',
|
|
573
|
+
},
|
|
574
|
+
body: JSON.stringify(requestBody),
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
if (!response.ok) {
|
|
578
|
+
const errorText = await response.text();
|
|
579
|
+
throw new Error(`Provider API error: ${response.status} - ${errorText}`);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const data = await response.json();
|
|
583
|
+
const content = data.choices?.[0]?.message?.content
|
|
584
|
+
|| data.choices?.[0]?.text
|
|
585
|
+
|| data.response
|
|
586
|
+
|| data.content
|
|
587
|
+
|| '';
|
|
588
|
+
// Maliyet takibi — usage döndüren her çağrı kaydedilir (natureco cost)
|
|
589
|
+
recordUsageSafe(providerConfig, data.usage);
|
|
590
|
+
return {
|
|
591
|
+
role: 'assistant',
|
|
592
|
+
content,
|
|
593
|
+
tool_calls: data.choices?.[0]?.message?.tool_calls || undefined,
|
|
594
|
+
usage: data.usage || undefined,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Kullanımı cost-tracker'a kaydet; takip hatası asıl akışı asla bozmasın.
|
|
600
|
+
*/
|
|
601
|
+
function recordUsageSafe(providerConfig, usage) {
|
|
602
|
+
if (!usage) return;
|
|
603
|
+
try {
|
|
604
|
+
const { recordUsage } = require('./cost-tracker');
|
|
605
|
+
recordUsage({
|
|
606
|
+
provider: detectProvider(providerConfig.url, providerConfig.model),
|
|
607
|
+
model: providerConfig.model,
|
|
608
|
+
input: usage.prompt_tokens ?? usage.input_tokens ?? 0,
|
|
609
|
+
output: usage.completion_tokens ?? usage.output_tokens ?? 0,
|
|
610
|
+
command: process.argv.slice(2).find(a => !a.startsWith('-')) || null,
|
|
611
|
+
});
|
|
612
|
+
} catch { /* takip hatası sessiz geçilir */ }
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Send message to Anthropic API
|
|
617
|
+
*/
|
|
618
|
+
async function sendMessageAnthropic(providerConfig, messages, tools) {
|
|
619
|
+
const endpoint = `${providerConfig.url}/v1/messages`;
|
|
620
|
+
|
|
621
|
+
// Anthropic requires system message separate; never send empty string.
|
|
622
|
+
const userMessages = messages.filter(m => m.role !== 'system');
|
|
623
|
+
|
|
624
|
+
const response = await fetch(endpoint, {
|
|
625
|
+
method: 'POST',
|
|
626
|
+
headers: {
|
|
627
|
+
'x-api-key': providerConfig.apiKey,
|
|
628
|
+
'anthropic-version': '2023-06-01',
|
|
629
|
+
'Content-Type': 'application/json',
|
|
630
|
+
},
|
|
631
|
+
body: JSON.stringify({
|
|
632
|
+
model: providerConfig.model,
|
|
633
|
+
max_tokens: 2000,
|
|
634
|
+
system: extractSystemForAnthropic(messages),
|
|
635
|
+
messages: userMessages,
|
|
636
|
+
tools: tools,
|
|
637
|
+
}),
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
if (!response.ok) {
|
|
641
|
+
const errorText = await response.text();
|
|
642
|
+
throw new Error(`Anthropic API error: ${response.status} - ${errorText}`);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const data = await response.json();
|
|
646
|
+
|
|
647
|
+
// Convert Anthropic response to OpenAI format
|
|
648
|
+
const content = data.content.find(c => c.type === 'text')?.text || '';
|
|
649
|
+
const toolCalls = data.content
|
|
650
|
+
.filter(c => c.type === 'tool_use')
|
|
651
|
+
.map(c => ({
|
|
652
|
+
id: c.id,
|
|
653
|
+
type: 'function',
|
|
654
|
+
function: {
|
|
655
|
+
name: c.name,
|
|
656
|
+
arguments: JSON.stringify(c.input)
|
|
657
|
+
}
|
|
658
|
+
}));
|
|
659
|
+
|
|
660
|
+
// Maliyet takibi (Anthropic: input_tokens/output_tokens)
|
|
661
|
+
recordUsageSafe(providerConfig, data.usage);
|
|
662
|
+
|
|
663
|
+
return {
|
|
664
|
+
role: 'assistant',
|
|
665
|
+
content: content,
|
|
666
|
+
tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
667
|
+
usage: data.usage || undefined,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Send message with tool support (universal)
|
|
673
|
+
*/
|
|
674
|
+
async function sendMessageToProvider(apiKey, message, conversationId = null, systemPrompt = null, options = {}) {
|
|
675
|
+
const providerConfig = getProviderConfig();
|
|
676
|
+
|
|
677
|
+
if (!providerConfig) {
|
|
678
|
+
throw new Error(
|
|
679
|
+
'Provider not configured. Set with:\n' +
|
|
680
|
+
' natureco config set providerUrl https://api.groq.com/openai/v1\n' +
|
|
681
|
+
' natureco config set providerApiKey gsk_xxx\n' +
|
|
682
|
+
' natureco config set providerModel llama-3.3-70b-versatile'
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Start MCP servers if not already started
|
|
687
|
+
if (Object.keys(mcpClients).length === 0) {
|
|
688
|
+
await startMcpServers();
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Get or create conversation history (load from disk)
|
|
692
|
+
// Use consistent ID based on provider config instead of timestamp
|
|
693
|
+
const convId = conversationId || generateDefaultConvId();
|
|
694
|
+
const history = loadConversation(convId);
|
|
695
|
+
|
|
696
|
+
// Augment system prompt with project AGENTS.md instructions
|
|
697
|
+
const agentsMd = require('./agents-md');
|
|
698
|
+
const augmentedPrompt = agentsMd.injectIntoPrompt(systemPrompt || '', options?.cwd || process.cwd());
|
|
699
|
+
|
|
700
|
+
// Build messages
|
|
701
|
+
let messages = [];
|
|
702
|
+
if (augmentedPrompt) {
|
|
703
|
+
messages.push({ role: 'system', content: augmentedPrompt });
|
|
704
|
+
}
|
|
705
|
+
messages.push(...history);
|
|
706
|
+
messages.push({ role: 'user', content: message });
|
|
707
|
+
|
|
708
|
+
// Get tool definitions (local + MCP) — skip if noTools flag set (chat mode)
|
|
709
|
+
const tools = options.noTools
|
|
710
|
+
? []
|
|
711
|
+
: (providerConfig.isAnthropic ? formatToolsForAnthropic() : formatToolsForOpenAI());
|
|
712
|
+
|
|
713
|
+
debugLog('\n[Provider] Sending request...');
|
|
714
|
+
debugLog('[Provider] URL:', providerConfig.url);
|
|
715
|
+
debugLog('[Provider] Model:', providerConfig.model);
|
|
716
|
+
debugLog('[Provider] Type:', providerConfig.isAnthropic ? 'Anthropic' : 'OpenAI-compatible');
|
|
717
|
+
debugLog('[Provider] Messages:', messages.length);
|
|
718
|
+
debugLog('[Provider] Tools:', tools.length, `(${Object.keys(mcpClients).length} MCP servers)`);
|
|
719
|
+
|
|
720
|
+
// Tool execution loop (max 10 iterations)
|
|
721
|
+
let iteration = 0;
|
|
722
|
+
const maxIterations = 10;
|
|
723
|
+
let finalResponse = null;
|
|
724
|
+
const stream = (options.stream ?? options.noStream === undefined) !== false &&
|
|
725
|
+
!providerConfig.url.includes('api.natureco.me');
|
|
726
|
+
|
|
727
|
+
while (iteration < maxIterations) {
|
|
728
|
+
iteration++;
|
|
729
|
+
debugLog(`\n[Provider] Iteration ${iteration}/${maxIterations}`);
|
|
730
|
+
|
|
731
|
+
let assistantMessage;
|
|
732
|
+
|
|
733
|
+
if (stream) {
|
|
734
|
+
const result = await streamProviderCompletion(providerConfig, messages, tools);
|
|
735
|
+
if (result.type === 'text') {
|
|
736
|
+
messages.push({ role: 'assistant', content: result.content });
|
|
737
|
+
finalResponse = result.content;
|
|
738
|
+
break;
|
|
739
|
+
}
|
|
740
|
+
assistantMessage = result.message;
|
|
741
|
+
} else {
|
|
742
|
+
assistantMessage = providerConfig.isAnthropic
|
|
743
|
+
? await sendMessageAnthropic(providerConfig, messages, tools)
|
|
744
|
+
: await sendMessageOpenAICompatible(providerConfig, messages, tools);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (!assistantMessage) {
|
|
748
|
+
return {
|
|
749
|
+
reply: 'No response from provider',
|
|
750
|
+
conversation_id: convId,
|
|
751
|
+
message_id: `msg_${Date.now()}`,
|
|
752
|
+
success: false
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Track token usage if available
|
|
757
|
+
if (assistantMessage.usage) {
|
|
758
|
+
TB.trackUsage(convId, {
|
|
759
|
+
input: assistantMessage.usage.prompt_tokens || assistantMessage.usage.input_tokens || 0,
|
|
760
|
+
output: assistantMessage.usage.completion_tokens || assistantMessage.usage.output_tokens || 0
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
debugLog('[Provider] Response type:', assistantMessage?.tool_calls ? 'tool_calls' : 'text');
|
|
765
|
+
|
|
766
|
+
// Add assistant message to history
|
|
767
|
+
messages.push(assistantMessage);
|
|
768
|
+
|
|
769
|
+
// Check for tool calls
|
|
770
|
+
const hasToolCalls = assistantMessage?.tool_calls?.length > 0;
|
|
771
|
+
if (hasToolCalls) {
|
|
772
|
+
debugLog(`[Provider] Tool calls: ${assistantMessage.tool_calls.length}`);
|
|
773
|
+
|
|
774
|
+
// Separate local and MCP tool calls
|
|
775
|
+
const toolCalls = assistantMessage.tool_calls.map(tc => ({
|
|
776
|
+
id: tc.id,
|
|
777
|
+
name: tc.function.name,
|
|
778
|
+
input: JSON.parse(tc.function.arguments)
|
|
779
|
+
}));
|
|
780
|
+
|
|
781
|
+
// Group MCP and local tools
|
|
782
|
+
const mcpTools = getMcpTools();
|
|
783
|
+
const mcpCalls = toolCalls.filter(tc => mcpTools.find(t => t.name === tc.name));
|
|
784
|
+
const localCalls = toolCalls.filter(tc => !mcpTools.find(t => t.name === tc.name));
|
|
785
|
+
|
|
786
|
+
// Guardrails: filter blocked tools
|
|
787
|
+
guardrails.reset();
|
|
788
|
+
guardrails.startIteration();
|
|
789
|
+
const blockedMcp = mcpCalls.filter(tc => {
|
|
790
|
+
const check = guardrails.check(tc.name, tc.input);
|
|
791
|
+
return check.blocked;
|
|
792
|
+
});
|
|
793
|
+
const blockedLocal = localCalls.filter(tc => {
|
|
794
|
+
const check = guardrails.check(tc.name, tc.input);
|
|
795
|
+
return check.blocked;
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
// Execute local tools in parallel (tool-runner already parallelizes safe tools)
|
|
799
|
+
let localResults = [];
|
|
800
|
+
if (localCalls.filter(tc => !blockedLocal.includes(tc)).length > 0) {
|
|
801
|
+
debugLog(`[Local] Executing ${localCalls.length} tool(s) concurrently`);
|
|
802
|
+
localResults = await executeToolCalls(
|
|
803
|
+
localCalls.filter(tc => !blockedLocal.includes(tc)),
|
|
804
|
+
{ toolDefinitions: getToolDefinitions() }
|
|
805
|
+
);
|
|
806
|
+
for (const r of localResults) {
|
|
807
|
+
guardrails.record(r.name, {}, r.result?.success !== false);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Execute MCP tools in parallel (they're independent by nature)
|
|
812
|
+
let mcpResults = [];
|
|
813
|
+
if (mcpCalls.filter(tc => !blockedMcp.includes(tc)).length > 0) {
|
|
814
|
+
debugLog(`[MCP] Executing ${mcpCalls.length} tool(s) concurrently`);
|
|
815
|
+
mcpResults = await Promise.all(
|
|
816
|
+
mcpCalls.filter(tc => !blockedMcp.includes(tc)).map(async (tc) => {
|
|
817
|
+
const result = await executeMcpTool(tc.name, tc.input);
|
|
818
|
+
guardrails.record(tc.name, tc.input, result?.success !== false);
|
|
819
|
+
return { id: tc.id, name: tc.name, result };
|
|
820
|
+
})
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Add blocked tool results as errors
|
|
825
|
+
const allResults = [
|
|
826
|
+
...blockedMcp.map(tc => ({ id: tc.id, name: tc.name, result: { error: `blocked_by_guardrails: ${tc.name}` } })),
|
|
827
|
+
...blockedLocal.map(tc => ({ id: tc.id, name: tc.name, result: { error: `blocked_by_guardrails: ${tc.name}` } })),
|
|
828
|
+
...localResults,
|
|
829
|
+
...mcpResults,
|
|
830
|
+
];
|
|
831
|
+
|
|
832
|
+
for (const result of allResults) {
|
|
833
|
+
const encodedContent = encodeToolResult(result.result);
|
|
834
|
+
messages.push({
|
|
835
|
+
role: 'tool',
|
|
836
|
+
tool_call_id: result.id,
|
|
837
|
+
name: result.name,
|
|
838
|
+
content: encodedContent
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Continue loop to get final response
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// No tool calls, we have final response
|
|
847
|
+
finalResponse = assistantMessage?.content;
|
|
848
|
+
break;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if (iteration >= maxIterations) {
|
|
852
|
+
debugLog('\n[Provider] Max iterations reached');
|
|
853
|
+
finalResponse = finalResponse || 'Max tool execution iterations reached.';
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Apply token budget trimming
|
|
857
|
+
TB.trimMessages(messages);
|
|
858
|
+
|
|
859
|
+
// Save to conversation history (only user and final assistant message)
|
|
860
|
+
history.push({ role: 'user', content: message });
|
|
861
|
+
history.push({ role: 'assistant', content: finalResponse });
|
|
862
|
+
|
|
863
|
+
// Save to disk (automatically keeps last 20 messages)
|
|
864
|
+
saveConversation(convId, history);
|
|
865
|
+
|
|
866
|
+
return {
|
|
867
|
+
reply: finalResponse,
|
|
868
|
+
conversation_id: convId,
|
|
869
|
+
message_id: `msg_${Date.now()}`,
|
|
870
|
+
success: true
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Clear conversation history
|
|
876
|
+
*/
|
|
877
|
+
function clearConversation(conversationId) {
|
|
878
|
+
if (conversationId) {
|
|
879
|
+
// Delete from disk
|
|
880
|
+
const file = path.join(CONV_DIR, `${conversationId.replace(/[^a-z0-9]/gi, '_')}.json`);
|
|
881
|
+
try {
|
|
882
|
+
if (fs.existsSync(file)) {
|
|
883
|
+
fs.unlinkSync(file);
|
|
884
|
+
}
|
|
885
|
+
} catch (e) {
|
|
886
|
+
// Silently fail
|
|
887
|
+
}
|
|
888
|
+
// Also clear from memory (legacy)
|
|
889
|
+
conversationHistory.delete(conversationId);
|
|
890
|
+
} else {
|
|
891
|
+
// Clear all conversations from disk
|
|
892
|
+
try {
|
|
893
|
+
if (fs.existsSync(CONV_DIR)) {
|
|
894
|
+
const files = fs.readdirSync(CONV_DIR);
|
|
895
|
+
files.forEach(file => {
|
|
896
|
+
fs.unlinkSync(path.join(CONV_DIR, file));
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
} catch (e) {
|
|
900
|
+
// Silently fail
|
|
901
|
+
}
|
|
902
|
+
// Also clear from memory (legacy)
|
|
903
|
+
conversationHistory.clear();
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Legacy function for compatibility
|
|
909
|
+
* Now supports custom system prompts for different platforms (terminal, WhatsApp, etc.)
|
|
910
|
+
* @param {string} chatSystemPrompt - System prompt from chat.js (skills + memory + agents)
|
|
911
|
+
*/
|
|
912
|
+
async function sendMessage(apiKey, botId, message, conversationId = null, chatSystemPrompt = '', options = {}) {
|
|
913
|
+
// Handle legacy 6th param (toolDefinitions array was passed)
|
|
914
|
+
if (Array.isArray(options)) options = {};
|
|
915
|
+
const providerConfig = getProviderConfig();
|
|
916
|
+
|
|
917
|
+
// Get user's home directory
|
|
918
|
+
const homeDir = os.homedir();
|
|
919
|
+
|
|
920
|
+
// Load memory to get botName
|
|
921
|
+
const { loadMemory } = require('./memory');
|
|
922
|
+
const mem = loadMemory(botId);
|
|
923
|
+
|
|
924
|
+
// Get config to check MCP status
|
|
925
|
+
const config = getConfig();
|
|
926
|
+
|
|
927
|
+
// NatureCo — minimal system prompt, skip tool descriptions/MCP
|
|
928
|
+
if (providerConfig && providerConfig.url.includes('api.natureco.me')) {
|
|
929
|
+
const prompt = chatSystemPrompt || 'Sen yardımcı bir AI asistansın.';
|
|
930
|
+
return sendMessageToProvider(apiKey, message, conversationId, prompt, options);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// Minimal base prompt (~200 token)
|
|
934
|
+
const toolDefs = getToolDefinitions();
|
|
935
|
+
const toolsDesc = toolDefs.map(t => t.name).join(', ');
|
|
936
|
+
let systemPrompt = `Assistant. Tools: ${toolsDesc}. Home: ${homeDir}.`;
|
|
937
|
+
|
|
938
|
+
// Skill prompts only, max 500 chars
|
|
939
|
+
if (chatSystemPrompt) {
|
|
940
|
+
systemPrompt += '\n' + chatSystemPrompt.slice(0, TB.load().systemPromptMaxChars);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
return sendMessageToProvider(apiKey, message, conversationId, systemPrompt, options);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Validate API key against NatureCo backend
|
|
948
|
+
* Returns { valid, error, user }
|
|
949
|
+
*/
|
|
950
|
+
async function validateApiKey(apiKey) {
|
|
951
|
+
const result = { valid: false, error: null, user: null };
|
|
952
|
+
if (!apiKey) {
|
|
953
|
+
result.error = 'API key boş olamaz';
|
|
954
|
+
return result;
|
|
955
|
+
}
|
|
956
|
+
try {
|
|
957
|
+
const res = await fetch('https://api.natureco.me/api/v1/user/me', {
|
|
958
|
+
headers: {
|
|
959
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
960
|
+
'Content-Type': 'application/json',
|
|
961
|
+
},
|
|
962
|
+
});
|
|
963
|
+
if (res.ok) {
|
|
964
|
+
const body = await res.json();
|
|
965
|
+
result.valid = true;
|
|
966
|
+
result.user = body.user || body.data || body;
|
|
967
|
+
return result;
|
|
968
|
+
}
|
|
969
|
+
let errorBody = '';
|
|
970
|
+
try { errorBody = await res.text(); } catch {}
|
|
971
|
+
result.error = `API doğrulama hatası (${res.status}): ${errorBody || 'Geçersiz API key'}`;
|
|
972
|
+
return result;
|
|
973
|
+
} catch (e) {
|
|
974
|
+
result.error = `Bağlantı hatası: ${e.message}`;
|
|
975
|
+
return result;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* Get bots (not used in v2.x, kept for compatibility)
|
|
981
|
+
*/
|
|
982
|
+
async function getBots(apiKey) {
|
|
983
|
+
const config = getConfig();
|
|
984
|
+
const providerConfig = getProviderConfig();
|
|
985
|
+
|
|
986
|
+
// NatureCo provider — gerçek bot listesini API'den çek
|
|
987
|
+
if (config.providerUrl && config.providerUrl.includes('natureco.me')) {
|
|
988
|
+
try {
|
|
989
|
+
const res = await fetch('https://api.natureco.me/api/v1/bots', {
|
|
990
|
+
headers: {
|
|
991
|
+
'Authorization': `Bearer ${config.providerApiKey || apiKey}`,
|
|
992
|
+
'Content-Type': 'application/json',
|
|
993
|
+
},
|
|
994
|
+
});
|
|
995
|
+
if (res.ok) {
|
|
996
|
+
const data = await res.json();
|
|
997
|
+
const bots = Array.isArray(data) ? data : (data.bots || data.data || []);
|
|
998
|
+
if (bots.length > 0) {
|
|
999
|
+
return {
|
|
1000
|
+
bots: bots.map(b => ({
|
|
1001
|
+
id: b.id,
|
|
1002
|
+
name: b.name,
|
|
1003
|
+
ai_provider: b.ai_provider || 'natureco',
|
|
1004
|
+
model: b.model || 'natureco-default',
|
|
1005
|
+
system_prompt: b.system_prompt || '',
|
|
1006
|
+
}))
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
} catch (e) {
|
|
1011
|
+
debugLog('[getBots] NatureCo API error:', e.message);
|
|
1012
|
+
return { bots: [], error: e.message };
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// Diğer provider'lar — universal provider döndür
|
|
1017
|
+
const providerName = providerConfig?.isAnthropic ? 'Anthropic' : 'OpenAI-compatible';
|
|
1018
|
+
const botName = config.botName || `Universal Provider (${providerName})`;
|
|
1019
|
+
return {
|
|
1020
|
+
bots: [
|
|
1021
|
+
{
|
|
1022
|
+
id: 'universal-provider',
|
|
1023
|
+
name: botName,
|
|
1024
|
+
ai_provider: providerName,
|
|
1025
|
+
model: providerConfig?.model || 'unknown'
|
|
1026
|
+
}
|
|
1027
|
+
]
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// ── Streaming Support ────────────────────────────────────────────────────────────
|
|
1032
|
+
|
|
1033
|
+
async function streamProviderCompletion(providerConfig, messages, tools) {
|
|
1034
|
+
if (providerConfig.isAnthropic) {
|
|
1035
|
+
return streamAnthropicCompletion(providerConfig, messages);
|
|
1036
|
+
}
|
|
1037
|
+
return streamOpenAICompletion(providerConfig, messages, tools);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
async function streamOpenAICompletion(providerConfig, messages, tools) {
|
|
1041
|
+
const baseUrl = providerConfig.url.replace(/\/+$/, '');
|
|
1042
|
+
// Tek doğruluk kaynağı: provider-detect.buildChatEndpoint (MiniMax /v1 toleransı dahil)
|
|
1043
|
+
const endpoint = buildChatEndpoint(baseUrl);
|
|
1044
|
+
|
|
1045
|
+
const requestBody = {
|
|
1046
|
+
model: providerConfig.model,
|
|
1047
|
+
messages,
|
|
1048
|
+
temperature: 0.7,
|
|
1049
|
+
max_tokens: 2000,
|
|
1050
|
+
stream: true,
|
|
1051
|
+
};
|
|
1052
|
+
if (tools && tools.length > 0) {
|
|
1053
|
+
requestBody.tools = tools;
|
|
1054
|
+
requestBody.tool_choice = 'auto';
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
const response = await fetch(endpoint, {
|
|
1058
|
+
method: 'POST',
|
|
1059
|
+
headers: {
|
|
1060
|
+
'Authorization': `Bearer ${providerConfig.apiKey}`,
|
|
1061
|
+
'Content-Type': 'application/json',
|
|
1062
|
+
},
|
|
1063
|
+
body: JSON.stringify(requestBody),
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
if (!response.ok) {
|
|
1067
|
+
throw new Error(`Provider API error: ${response.status} - ${await response.text()}`);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
const reader = response.body.getReader();
|
|
1071
|
+
const decoder = new TextDecoder();
|
|
1072
|
+
let fullText = '';
|
|
1073
|
+
const toolCalls = [];
|
|
1074
|
+
let hasToolCalls = false;
|
|
1075
|
+
let streamUsage = null;
|
|
1076
|
+
// SSE satırları TCP chunk sınırında bölünebilir — kuyruk buffer'da taşınmalı,
|
|
1077
|
+
// yoksa bölünen JSON parse edilemez ve token sessizce kaybolur
|
|
1078
|
+
let sseBuffer = '';
|
|
1079
|
+
|
|
1080
|
+
while (true) {
|
|
1081
|
+
const { done, value } = await reader.read();
|
|
1082
|
+
if (done) break;
|
|
1083
|
+
|
|
1084
|
+
sseBuffer += decoder.decode(value, { stream: true });
|
|
1085
|
+
const parts = sseBuffer.split('\n');
|
|
1086
|
+
sseBuffer = parts.pop() || ''; // son (muhtemelen yarım) satırı sakla
|
|
1087
|
+
const lines = parts.filter(l => l.startsWith('data: '));
|
|
1088
|
+
|
|
1089
|
+
for (const line of lines) {
|
|
1090
|
+
const data = line.slice(6).trim();
|
|
1091
|
+
if (data === '[DONE]') continue;
|
|
1092
|
+
try {
|
|
1093
|
+
const parsed = JSON.parse(data);
|
|
1094
|
+
if (parsed.usage) streamUsage = parsed.usage; // son chunk'ta gelir
|
|
1095
|
+
const delta = parsed.choices?.[0]?.delta;
|
|
1096
|
+
if (!delta) continue;
|
|
1097
|
+
|
|
1098
|
+
if (delta.tool_calls) {
|
|
1099
|
+
hasToolCalls = true;
|
|
1100
|
+
accumulateToolCallDeltas(toolCalls, delta.tool_calls);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const token = delta.content || '';
|
|
1104
|
+
if (token) {
|
|
1105
|
+
if (!hasToolCalls) process.stdout.write(token);
|
|
1106
|
+
fullText += token;
|
|
1107
|
+
}
|
|
1108
|
+
} catch {}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
// Maliyet takibi — sağlayıcı stream sonunda usage gönderdiyse kaydet
|
|
1112
|
+
recordUsageSafe(providerConfig, streamUsage);
|
|
1113
|
+
|
|
1114
|
+
if (hasToolCalls) {
|
|
1115
|
+
process.stdout.write('\n');
|
|
1116
|
+
return {
|
|
1117
|
+
type: 'tool_calls',
|
|
1118
|
+
message: {
|
|
1119
|
+
role: 'assistant',
|
|
1120
|
+
content: fullText || null,
|
|
1121
|
+
tool_calls: toolCalls
|
|
1122
|
+
.filter(tc => tc && tc.function && tc.function.name)
|
|
1123
|
+
.map(tc => ({
|
|
1124
|
+
id: tc.id || `call_${Date.now()}_${tc.index}`,
|
|
1125
|
+
type: tc.type || 'function',
|
|
1126
|
+
function: { name: tc.function.name, arguments: tc.function.arguments || '' }
|
|
1127
|
+
}))
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
process.stdout.write('\n');
|
|
1133
|
+
return { type: 'text', content: fullText };
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
async function streamAnthropicCompletion(providerConfig, messages) {
|
|
1137
|
+
const endpoint = `${providerConfig.url}/v1/messages`;
|
|
1138
|
+
|
|
1139
|
+
const userMessages = messages.filter(m => m.role !== 'system');
|
|
1140
|
+
|
|
1141
|
+
const response = await fetch(endpoint, {
|
|
1142
|
+
method: 'POST',
|
|
1143
|
+
headers: {
|
|
1144
|
+
'x-api-key': providerConfig.apiKey,
|
|
1145
|
+
'anthropic-version': '2023-06-01',
|
|
1146
|
+
'Content-Type': 'application/json',
|
|
1147
|
+
},
|
|
1148
|
+
body: JSON.stringify({
|
|
1149
|
+
model: providerConfig.model,
|
|
1150
|
+
max_tokens: 2000,
|
|
1151
|
+
system: extractSystemForAnthropic(messages),
|
|
1152
|
+
messages: userMessages,
|
|
1153
|
+
stream: true,
|
|
1154
|
+
}),
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
if (!response.ok) {
|
|
1158
|
+
throw new Error(`Anthropic API error: ${response.status} - ${await response.text()}`);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
const reader = response.body.getReader();
|
|
1162
|
+
const decoder = new TextDecoder();
|
|
1163
|
+
let fullText = '';
|
|
1164
|
+
|
|
1165
|
+
while (true) {
|
|
1166
|
+
const { done, value } = await reader.read();
|
|
1167
|
+
if (done) break;
|
|
1168
|
+
|
|
1169
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
1170
|
+
const lines = chunk.split('\n').filter(l => l.startsWith('data: '));
|
|
1171
|
+
|
|
1172
|
+
for (const line of lines) {
|
|
1173
|
+
const data = line.slice(6).trim();
|
|
1174
|
+
if (data === '[DONE]') continue;
|
|
1175
|
+
try {
|
|
1176
|
+
const parsed = JSON.parse(data);
|
|
1177
|
+
const token = parsed.delta?.text || '';
|
|
1178
|
+
if (token) {
|
|
1179
|
+
process.stdout.write(token);
|
|
1180
|
+
fullText += token;
|
|
1181
|
+
}
|
|
1182
|
+
} catch {}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
process.stdout.write('\n');
|
|
1187
|
+
return { type: 'text', content: fullText };
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
module.exports = {
|
|
1191
|
+
sendMessage,
|
|
1192
|
+
sendMessageToProvider,
|
|
1193
|
+
validateApiKey,
|
|
1194
|
+
getBots,
|
|
1195
|
+
clearConversation,
|
|
1196
|
+
getProviderConfig,
|
|
1197
|
+
startMcpServers,
|
|
1198
|
+
stopMcpServers,
|
|
1199
|
+
getMcpTools,
|
|
1200
|
+
streamProviderCompletion,
|
|
1201
|
+
streamOpenAICompletion,
|
|
1202
|
+
streamAnthropicCompletion,
|
|
1203
|
+
// Exposed for tests + advanced consumers (does not appear in the
|
|
1204
|
+
// public API surface of natureco's user-facing docs).
|
|
1205
|
+
_internals: {
|
|
1206
|
+
extractSystemForAnthropic,
|
|
1207
|
+
DEFAULT_ANTHROPIC_SYSTEM,
|
|
1208
|
+
buildRequestBody,
|
|
1209
|
+
},
|
|
1210
|
+
_sendMessage: sendMessage,
|
|
1211
|
+
};
|