@makeshkumar/blueorch-studio 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/bin/cli.js +69 -0
  2. package/cache-manager.js +191 -0
  3. package/chat.routes.js +691 -0
  4. package/llm.routes.js +285 -0
  5. package/log.routes.js +42 -0
  6. package/logger.js +86 -0
  7. package/mcp.routes.js +320 -0
  8. package/package.json +37 -0
  9. package/public/3rdpartylicenses.txt +416 -0
  10. package/public/browser/assets/monaco/_commonjsHelpers-CT9FvmAN.js +1 -0
  11. package/public/browser/assets/monaco/abap-D-t0cyap.js +1 -0
  12. package/public/browser/assets/monaco/apex-CcIm7xu6.js +1 -0
  13. package/public/browser/assets/monaco/assets/css.worker-HnVq6Ewq.js +93 -0
  14. package/public/browser/assets/monaco/assets/editor.worker-Be8ye1pW.js +26 -0
  15. package/public/browser/assets/monaco/assets/html.worker-B51mlPHg.js +470 -0
  16. package/public/browser/assets/monaco/assets/json.worker-DKiEKt88.js +58 -0
  17. package/public/browser/assets/monaco/assets/ts.worker-CMbG-7ft.js +67731 -0
  18. package/public/browser/assets/monaco/azcli-BA0tQDCg.js +1 -0
  19. package/public/browser/assets/monaco/basic-languages/monaco.contribution.js +1 -0
  20. package/public/browser/assets/monaco/bat-C397hTD6.js +1 -0
  21. package/public/browser/assets/monaco/bicep-DF5aW17k.js +2 -0
  22. package/public/browser/assets/monaco/cameligo-plsz8qhj.js +1 -0
  23. package/public/browser/assets/monaco/clojure-Y2auQMzK.js +1 -0
  24. package/public/browser/assets/monaco/coffee-Bu45yuWE.js +1 -0
  25. package/public/browser/assets/monaco/cpp-CkKPQIni.js +1 -0
  26. package/public/browser/assets/monaco/csharp-CX28MZyh.js +1 -0
  27. package/public/browser/assets/monaco/csp-D8uWnyxW.js +1 -0
  28. package/public/browser/assets/monaco/css-CaeNmE3S.js +3 -0
  29. package/public/browser/assets/monaco/cssMode-CjiAH6dQ.js +1 -0
  30. package/public/browser/assets/monaco/cypher-DVThT8BS.js +1 -0
  31. package/public/browser/assets/monaco/dart-CmGfCvrO.js +1 -0
  32. package/public/browser/assets/monaco/dockerfile-CZqqYdch.js +1 -0
  33. package/public/browser/assets/monaco/ecl-30fUercY.js +1 -0
  34. package/public/browser/assets/monaco/editor/editor.main.css +1 -0
  35. package/public/browser/assets/monaco/editor/editor.main.js +5 -0
  36. package/public/browser/assets/monaco/editor.api-CalNCsUg.js +903 -0
  37. package/public/browser/assets/monaco/elixir-xjPaIfzF.js +1 -0
  38. package/public/browser/assets/monaco/flow9-DqtmStfK.js +1 -0
  39. package/public/browser/assets/monaco/freemarker2-Cz_sV6Md.js +3 -0
  40. package/public/browser/assets/monaco/fsharp-BOMdg4U1.js +1 -0
  41. package/public/browser/assets/monaco/go-D_hbi-Jt.js +1 -0
  42. package/public/browser/assets/monaco/graphql-CKUU4kLG.js +1 -0
  43. package/public/browser/assets/monaco/handlebars-OwglfO-1.js +1 -0
  44. package/public/browser/assets/monaco/hcl-DTaboeZW.js +1 -0
  45. package/public/browser/assets/monaco/html-Pa1xEWsY.js +1 -0
  46. package/public/browser/assets/monaco/htmlMode-Bz67EXwp.js +1 -0
  47. package/public/browser/assets/monaco/ini-CsNwO04R.js +1 -0
  48. package/public/browser/assets/monaco/java-CI4ZMsH9.js +1 -0
  49. package/public/browser/assets/monaco/javascript-PczUCGdz.js +1 -0
  50. package/public/browser/assets/monaco/jsonMode-DULH5oaX.js +7 -0
  51. package/public/browser/assets/monaco/julia-BwzEvaQw.js +1 -0
  52. package/public/browser/assets/monaco/kotlin-IUYPiTV8.js +1 -0
  53. package/public/browser/assets/monaco/language/css/monaco.contribution.js +1 -0
  54. package/public/browser/assets/monaco/language/html/monaco.contribution.js +1 -0
  55. package/public/browser/assets/monaco/language/json/monaco.contribution.js +1 -0
  56. package/public/browser/assets/monaco/language/typescript/monaco.contribution.js +1 -0
  57. package/public/browser/assets/monaco/less-C0eDYdqa.js +2 -0
  58. package/public/browser/assets/monaco/lexon-iON-Kj97.js +1 -0
  59. package/public/browser/assets/monaco/liquid-DqKjdPGy.js +1 -0
  60. package/public/browser/assets/monaco/loader.js +1368 -0
  61. package/public/browser/assets/monaco/lspLanguageFeatures-kM9O9rjY.js +4 -0
  62. package/public/browser/assets/monaco/lua-DtygF91M.js +1 -0
  63. package/public/browser/assets/monaco/m3-CsR4AuFi.js +1 -0
  64. package/public/browser/assets/monaco/markdown-C_rD0bIw.js +1 -0
  65. package/public/browser/assets/monaco/mdx-DEWtB1K5.js +1 -0
  66. package/public/browser/assets/monaco/mips-CiYP61RB.js +1 -0
  67. package/public/browser/assets/monaco/monaco.contribution-D2OdxNBt.js +1 -0
  68. package/public/browser/assets/monaco/monaco.contribution-DO3azKX8.js +1 -0
  69. package/public/browser/assets/monaco/monaco.contribution-EcChJV6a.js +1 -0
  70. package/public/browser/assets/monaco/monaco.contribution-qLAYrEOP.js +1 -0
  71. package/public/browser/assets/monaco/msdax-C38-sJlp.js +1 -0
  72. package/public/browser/assets/monaco/mysql-CdtbpvbG.js +1 -0
  73. package/public/browser/assets/monaco/nls.messages-loader.js +1 -0
  74. package/public/browser/assets/monaco/nls.messages.cs.js.js +17 -0
  75. package/public/browser/assets/monaco/nls.messages.de.js.js +17 -0
  76. package/public/browser/assets/monaco/nls.messages.es.js.js +17 -0
  77. package/public/browser/assets/monaco/nls.messages.fr.js.js +15 -0
  78. package/public/browser/assets/monaco/nls.messages.it.js.js +15 -0
  79. package/public/browser/assets/monaco/nls.messages.ja.js.js +17 -0
  80. package/public/browser/assets/monaco/nls.messages.js.js +10 -0
  81. package/public/browser/assets/monaco/nls.messages.ko.js.js +25 -0
  82. package/public/browser/assets/monaco/nls.messages.pl.js.js +17 -0
  83. package/public/browser/assets/monaco/nls.messages.pt-br.js.js +6 -0
  84. package/public/browser/assets/monaco/nls.messages.ru.js.js +17 -0
  85. package/public/browser/assets/monaco/nls.messages.tr.js.js +15 -0
  86. package/public/browser/assets/monaco/nls.messages.zh-cn.js.js +17 -0
  87. package/public/browser/assets/monaco/nls.messages.zh-tw.js.js +15 -0
  88. package/public/browser/assets/monaco/objective-c-CntZFaHX.js +1 -0
  89. package/public/browser/assets/monaco/pascal-r6kuqfl_.js +1 -0
  90. package/public/browser/assets/monaco/pascaligo-BiXoTmXh.js +1 -0
  91. package/public/browser/assets/monaco/perl-DABw_TcH.js +1 -0
  92. package/public/browser/assets/monaco/pgsql-me_jFXeX.js +1 -0
  93. package/public/browser/assets/monaco/php-D_kh-9LK.js +1 -0
  94. package/public/browser/assets/monaco/pla-VfZjczW0.js +1 -0
  95. package/public/browser/assets/monaco/postiats-BBSzz8Pk.js +1 -0
  96. package/public/browser/assets/monaco/powerquery-Dt-g_2cc.js +1 -0
  97. package/public/browser/assets/monaco/powershell-B-7ap1zc.js +1 -0
  98. package/public/browser/assets/monaco/protobuf-BmtuEB1A.js +2 -0
  99. package/public/browser/assets/monaco/pug-BRpRNeEb.js +1 -0
  100. package/public/browser/assets/monaco/python-Cr0UkIbn.js +1 -0
  101. package/public/browser/assets/monaco/qsharp-BzsFaUU9.js +1 -0
  102. package/public/browser/assets/monaco/r-f8dDdrp4.js +1 -0
  103. package/public/browser/assets/monaco/razor-BYAHOTkz.js +1 -0
  104. package/public/browser/assets/monaco/redis-fvZQY4PI.js +1 -0
  105. package/public/browser/assets/monaco/redshift-45Et0LQi.js +1 -0
  106. package/public/browser/assets/monaco/restructuredtext-C7UUFKFD.js +1 -0
  107. package/public/browser/assets/monaco/ruby-CZO8zYTz.js +1 -0
  108. package/public/browser/assets/monaco/rust-Bfetafyc.js +1 -0
  109. package/public/browser/assets/monaco/sb-3GYllVck.js +1 -0
  110. package/public/browser/assets/monaco/scala-foMgrKo1.js +1 -0
  111. package/public/browser/assets/monaco/scheme-CHdMtr7p.js +1 -0
  112. package/public/browser/assets/monaco/scss-C1cmLt9V.js +3 -0
  113. package/public/browser/assets/monaco/shell-ClXCKCEW.js +1 -0
  114. package/public/browser/assets/monaco/solidity-MZ6ExpPy.js +1 -0
  115. package/public/browser/assets/monaco/sophia-DWkuSsPQ.js +1 -0
  116. package/public/browser/assets/monaco/sparql-AUGFYSyk.js +1 -0
  117. package/public/browser/assets/monaco/sql-32GpJSV2.js +1 -0
  118. package/public/browser/assets/monaco/st-CuDFIVZ_.js +1 -0
  119. package/public/browser/assets/monaco/swift-n-2HociN.js +3 -0
  120. package/public/browser/assets/monaco/systemverilog-Ch4vA8Yt.js +1 -0
  121. package/public/browser/assets/monaco/tcl-D74tq1nH.js +1 -0
  122. package/public/browser/assets/monaco/tsMode-CZz1Umrk.js +11 -0
  123. package/public/browser/assets/monaco/twig-C6taOxMV.js +1 -0
  124. package/public/browser/assets/monaco/typescript-DfOrAzoV.js +1 -0
  125. package/public/browser/assets/monaco/typespec-D-PIh9Xw.js +1 -0
  126. package/public/browser/assets/monaco/vb-Dyb2648j.js +1 -0
  127. package/public/browser/assets/monaco/wgsl-BhLXMOR0.js +298 -0
  128. package/public/browser/assets/monaco/workers-DcJshg-q.js +1 -0
  129. package/public/browser/assets/monaco/xml-CdsdnY8S.js +1 -0
  130. package/public/browser/assets/monaco/yaml-DYGvmE88.js +1 -0
  131. package/public/browser/favicon.ico +0 -0
  132. package/public/browser/favicon.svg +4 -0
  133. package/public/browser/index.html +20 -0
  134. package/public/browser/main-BRU65EMU.js +80 -0
  135. package/public/browser/polyfills-FFHMD2TL.js +2 -0
  136. package/public/browser/styles-R37AZPY2.css +1 -0
  137. package/server.js +60 -0
  138. package/system.routes.js +150 -0
  139. package/usage-normalizer.js +117 -0
package/chat.routes.js ADDED
@@ -0,0 +1,691 @@
1
+ import { Router } from 'express';
2
+ import { isAbsolute, join } from 'path';
3
+ import { connectedLlmRegistry } from './llm.routes.js';
4
+ import { activeClients } from './mcp.routes.js';
5
+ import { getOrCreateCache, truncateIfLarge } from './cache-manager.js';
6
+ import { mapProviderUsage } from './usage-normalizer.js';
7
+
8
+ const router = Router();
9
+ const ts = () => new Date(Date.now() + (5 * 60 + 30) * 60000).toISOString().replace('Z', '+05:30');
10
+ const PATH_KEY_PATTERN = /(path|file|dir|directory|folder|filename)$/i;
11
+ const PATH_TEXT_PATTERN = /(absolute path|relative path|file path|directory path|folder path|workspace path|file name|filename|directory|folder|path)/i;
12
+
13
+ // ─── In-memory chat session (Phase 3: single session) ─────────────────────────
14
+ // Format: [{ role: 'user' | 'assistant', content: string }]
15
+ const chatHistory = [];
16
+
17
+ // ─── Schema helpers ────────────────────────────────────────────────────────────
18
+
19
+ /** Convert JSON Schema type string to Gemini's uppercase schema type. */
20
+ function toGeminiType(jsType) {
21
+ const map = {
22
+ string: 'STRING', number: 'NUMBER', integer: 'INTEGER',
23
+ boolean: 'BOOLEAN', array: 'ARRAY', object: 'OBJECT',
24
+ };
25
+ return map[(jsType || 'string').toLowerCase()] ?? 'STRING';
26
+ }
27
+
28
+ /** Recursively convert a JSON Schema object to Gemini FunctionDeclarationSchema. */
29
+ function toGeminiSchema(schema) {
30
+ if (!schema) return { type: 'OBJECT', properties: {} };
31
+ const out = { type: toGeminiType(schema.type) };
32
+ if (schema.description) out.description = schema.description;
33
+ if (schema.enum) out.enum = schema.enum;
34
+ if (schema.properties) {
35
+ out.properties = {};
36
+ for (const [k, v] of Object.entries(schema.properties)) {
37
+ out.properties[k] = toGeminiSchema(v);
38
+ }
39
+ }
40
+ if (schema.required) out.required = schema.required;
41
+ if (schema.items) out.items = toGeminiSchema(schema.items);
42
+ return out;
43
+ }
44
+
45
+ /** Extract readable text from an MCP callTool result object.
46
+ * Applies token-safety truncation if the output exceeds 10 000 chars. */
47
+ function extractToolText(result) {
48
+ // MCP result: { content: [{ type: 'text', text: '...' }] }
49
+ let text;
50
+ if (Array.isArray(result?.content)) {
51
+ text = result.content.map(c => c.text ?? JSON.stringify(c)).join('\n');
52
+ } else {
53
+ text = JSON.stringify(result ?? '');
54
+ }
55
+ return truncateIfLarge(text);
56
+ }
57
+
58
+ /** Race an async operation against an AbortSignal.
59
+ * This prevents long-running provider calls from continuing app flow after client stop. */
60
+ function withAbort(promise, abortSignal) {
61
+ if (!abortSignal) return promise;
62
+ if (abortSignal.aborted) throw new Error('Request cancelled by client');
63
+
64
+ return new Promise((resolve, reject) => {
65
+ const onAbort = () => reject(new Error('Request cancelled by client'));
66
+ abortSignal.addEventListener('abort', onAbort, { once: true });
67
+
68
+ promise.then(
69
+ (value) => {
70
+ abortSignal.removeEventListener('abort', onAbort);
71
+ resolve(value);
72
+ },
73
+ (err) => {
74
+ abortSignal.removeEventListener('abort', onAbort);
75
+ reject(err);
76
+ }
77
+ );
78
+ });
79
+ }
80
+
81
+ function isPathLikeSchemaProperty(key, schemaProperty) {
82
+ const normalizedKey = (key ?? '').replace(/[_-]/g, '').toLowerCase();
83
+ const descriptionText = [
84
+ schemaProperty?.description,
85
+ schemaProperty?.title,
86
+ ].filter(Boolean).join(' ');
87
+ const schemaType = schemaProperty?.type;
88
+
89
+ if (schemaType && schemaType !== 'string') {
90
+ return false;
91
+ }
92
+
93
+ if (normalizedKey === 'content') {
94
+ return false;
95
+ }
96
+
97
+ return PATH_KEY_PATTERN.test(normalizedKey) || PATH_TEXT_PATTERN.test(descriptionText);
98
+ }
99
+
100
+ function getPathArgumentKeys(toolArgs, inputSchema) {
101
+ const schemaProperties = inputSchema?.properties ?? {};
102
+ const keys = new Set();
103
+
104
+ for (const [key, schemaProperty] of Object.entries(schemaProperties)) {
105
+ if (isPathLikeSchemaProperty(key, schemaProperty)) {
106
+ keys.add(key);
107
+ }
108
+ }
109
+
110
+ for (const [key, value] of Object.entries(toolArgs ?? {})) {
111
+ if (typeof value !== 'string') continue;
112
+ if (isPathLikeSchemaProperty(key, schemaProperties[key])) {
113
+ keys.add(key);
114
+ }
115
+ }
116
+
117
+ return [...keys];
118
+ }
119
+
120
+ function resolveWorkspacePathValue(pathValue, activeWorkspacePath) {
121
+ console.log(`[INIT] ${ts()} resolveWorkspacePathValue()`);
122
+
123
+ if (!activeWorkspacePath || typeof pathValue !== 'string') {
124
+ console.log(`[SUCCESS] ${ts()} resolveWorkspacePathValue() | no rewrite`);
125
+ return pathValue;
126
+ }
127
+
128
+ const trimmedPath = pathValue.trim();
129
+
130
+ if (!trimmedPath || trimmedPath === '.' || trimmedPath === './') {
131
+ console.log(`[SUCCESS] ${ts()} resolveWorkspacePathValue() | workspace root injected`);
132
+ return activeWorkspacePath;
133
+ }
134
+
135
+ if (isAbsolute(trimmedPath)) {
136
+ console.log(`[SUCCESS] ${ts()} resolveWorkspacePathValue() | absolute path preserved`);
137
+ return trimmedPath;
138
+ }
139
+
140
+ const resolvedPath = join(activeWorkspacePath, trimmedPath);
141
+ console.log(`[SUCCESS] ${ts()} resolveWorkspacePathValue() | resolved: "${resolvedPath}"`);
142
+ return resolvedPath;
143
+ }
144
+
145
+ function resolveWorkspaceToolArgs(toolName, toolArgs, activeWorkspacePath, inputSchema) {
146
+ console.log(`[INIT] ${ts()} resolveWorkspaceToolArgs() | tool: ${toolName}`);
147
+
148
+ const finalArgs = { ...(toolArgs ?? {}) };
149
+
150
+ if (!activeWorkspacePath) {
151
+ console.log(`[SUCCESS] ${ts()} resolveWorkspaceToolArgs() | no active workspace`);
152
+ return finalArgs;
153
+ }
154
+
155
+ const keysToResolve = getPathArgumentKeys(finalArgs, inputSchema);
156
+
157
+ let didRewrite = false;
158
+
159
+ for (const key of keysToResolve) {
160
+ const nextValue = resolveWorkspacePathValue(finalArgs[key], activeWorkspacePath);
161
+ if (nextValue !== undefined && nextValue !== finalArgs[key]) {
162
+ finalArgs[key] = nextValue;
163
+ didRewrite = true;
164
+ }
165
+ }
166
+
167
+ console.log(
168
+ `[SUCCESS] ${ts()} resolveWorkspaceToolArgs() | tool: ${toolName} | rewritten: ${didRewrite}`
169
+ );
170
+ return finalArgs;
171
+ }
172
+
173
+ // ─── Provider handlers ─────────────────────────────────────────────────────────
174
+
175
+ /**
176
+ * Unified Gemini handler.
177
+ * - Expert Mode : pass systemContext, leave cacheName null.
178
+ * - Project Mode : pass cacheName (system prompt is baked into the cache).
179
+ */
180
+ async function handleGemini({ apiKey, model, message, history, toolDefs, executeTool, toolsUsed, systemContext, cacheName, abortSignal, ensureNotCancelled }) {
181
+ const startMs = Date.now();
182
+ console.log(`[INIT] ${ts()} handleGemini() | model: ${model} | cached: ${!!cacheName}`);
183
+
184
+ const { GoogleGenAI } = await import('@google/genai');
185
+ const ai = new GoogleGenAI({ apiKey });
186
+
187
+ const functionDeclarations = toolDefs.map(t => ({
188
+ name: t.name,
189
+ description: t.description ?? '',
190
+ parameters: toGeminiSchema(t.inputSchema),
191
+ }));
192
+
193
+ // ── Build request contents ────────────────────────────────────────────────
194
+ const contents = [
195
+ ...history.map(h => ({
196
+ role: h.role === 'assistant' ? 'model' : 'user',
197
+ parts: [{ text: h.content }],
198
+ })),
199
+ { role: 'user', parts: [{ text: message }] },
200
+ ];
201
+
202
+ // ── Build generation config ───────────────────────────────────────────────
203
+ const config = {};
204
+
205
+ if (cacheName) {
206
+ // Project Mode: system prompt is already inside the cache — do NOT repeat it
207
+ config.cachedContent = cacheName;
208
+ } else if (systemContext) {
209
+ // Expert Mode: send system prompt directly
210
+ config.systemInstruction = systemContext;
211
+ }
212
+
213
+ if (functionDeclarations.length > 0) {
214
+ config.tools = [{ functionDeclarations }];
215
+ }
216
+
217
+ // ── Agentic tool loop ────────────────────────────────────────────────────────
218
+ while (true) {
219
+ ensureNotCancelled();
220
+ const response = await withAbort(
221
+ ai.models.generateContent({ model, contents, config }),
222
+ abortSignal
223
+ );
224
+ ensureNotCancelled();
225
+ const parts = response.candidates?.[0]?.content?.parts ?? [];
226
+ const funcCalls = parts.filter(p => p.functionCall);
227
+
228
+ if (funcCalls.length === 0) {
229
+ const text = parts.find(p => p.text)?.text ?? '';
230
+ const latencyMs = Date.now() - startMs;
231
+ const standardizedUsage = mapProviderUsage(response.usageMetadata, 'gemini', { model, latencyMs });
232
+ console.log(`[SUCCESS] ${ts()} handleGemini() complete | latencyMs: ${latencyMs}`);
233
+ return { text, standardizedUsage };
234
+ }
235
+
236
+ // Append model's tool-call turn and continue loop
237
+ contents.push({ role: 'model', parts });
238
+
239
+ const responseParts = [];
240
+ for (const part of funcCalls) {
241
+ ensureNotCancelled();
242
+ const { name, args } = part.functionCall;
243
+ toolsUsed.push(name);
244
+ console.log(`[INIT] ${ts()} Gemini → tool: ${name} | args: ${JSON.stringify(args)}`);
245
+ try {
246
+ const raw = await executeTool(name, args);
247
+ const output = extractToolText(raw);
248
+ responseParts.push({ functionResponse: { name, response: { output } } });
249
+ console.log(`[SUCCESS] ${ts()} Tool "${name}" returned`);
250
+ } catch (err) {
251
+ responseParts.push({ functionResponse: { name, response: { error: err.message } } });
252
+ console.log(`[ERROR] ${ts()} Tool "${name}" failed: ${err.message}`);
253
+ }
254
+ }
255
+
256
+ contents.push({ role: 'user', parts: responseParts });
257
+ }
258
+ }
259
+
260
+ async function handleOpenAI({ apiKey, model, message, history, toolDefs, executeTool, toolsUsed, systemContext, abortSignal, ensureNotCancelled }) {
261
+ const startMs = Date.now();
262
+ console.log(`[INIT] ${ts()} handleOpenAI() | model: ${model}`);
263
+ const OpenAI = (await import('openai')).default;
264
+ const openai = new OpenAI({ apiKey });
265
+
266
+ const tools = toolDefs.map(t => ({
267
+ type: 'function',
268
+ function: {
269
+ name: t.name,
270
+ description: t.description ?? '',
271
+ parameters: t.inputSchema ?? { type: 'object', properties: {} },
272
+ },
273
+ }));
274
+
275
+ const messages = [
276
+ ...(systemContext ? [{ role: 'system', content: systemContext }] : []),
277
+ ...history.map(h => ({ role: h.role, content: h.content })),
278
+ { role: 'user', content: message },
279
+ ];
280
+
281
+ ensureNotCancelled();
282
+ let response = await openai.chat.completions.create({
283
+ model,
284
+ messages,
285
+ ...(tools.length > 0 && { tools, tool_choice: 'auto' }),
286
+ }, { signal: abortSignal });
287
+ ensureNotCancelled();
288
+
289
+ // ── Agentic tool loop ────────────────────────────────────────────────────────
290
+ while (response.choices[0].finish_reason === 'tool_calls') {
291
+ ensureNotCancelled();
292
+ const assistantMsg = response.choices[0].message;
293
+ messages.push(assistantMsg);
294
+
295
+ for (const tc of assistantMsg.tool_calls) {
296
+ ensureNotCancelled();
297
+ toolsUsed.push(tc.function.name);
298
+ console.log(`[INIT] ${ts()} OpenAI → tool: ${tc.function.name}`);
299
+ let content;
300
+ try {
301
+ const args = JSON.parse(tc.function.arguments);
302
+ const raw = await executeTool(tc.function.name, args);
303
+ content = extractToolText(raw);
304
+ console.log(`[SUCCESS] ${ts()} Tool "${tc.function.name}" returned`);
305
+ } catch (err) {
306
+ content = JSON.stringify({ error: err.message });
307
+ console.log(`[ERROR] ${ts()} Tool "${tc.function.name}" failed: ${err.message}`);
308
+ }
309
+ messages.push({ role: 'tool', tool_call_id: tc.id, content });
310
+ }
311
+
312
+ response = await openai.chat.completions.create({
313
+ model, messages, tools, tool_choice: 'auto',
314
+ }, { signal: abortSignal });
315
+ ensureNotCancelled();
316
+ }
317
+
318
+ const text = response.choices[0].message.content ?? '';
319
+ const latencyMs = Date.now() - startMs;
320
+ const standardizedUsage = mapProviderUsage(response.usage, 'openai', { model, latencyMs });
321
+ console.log(`[SUCCESS] ${ts()} handleOpenAI() complete | latencyMs: ${latencyMs}`);
322
+ return { text, standardizedUsage };
323
+ }
324
+
325
+ // ─────────────────────────────────────────────────────────────────────────────
326
+
327
+ async function handleClaude({ apiKey, model, message, history, toolDefs, executeTool, toolsUsed, systemContext, abortSignal, ensureNotCancelled }) {
328
+ const startMs = Date.now();
329
+ console.log(`[INIT] ${ts()} handleClaude() | model: ${model}`);
330
+ const Anthropic = (await import('@anthropic-ai/sdk')).default;
331
+ const client = new Anthropic({ apiKey });
332
+
333
+ const tools = toolDefs.map(t => ({
334
+ name: t.name,
335
+ description: t.description ?? '',
336
+ input_schema: t.inputSchema ?? { type: 'object', properties: {} },
337
+ }));
338
+
339
+ const messages = [
340
+ ...history.map(h => ({ role: h.role, content: h.content })),
341
+ { role: 'user', content: message },
342
+ ];
343
+
344
+ ensureNotCancelled();
345
+ let response = await client.messages.create({
346
+ model,
347
+ max_tokens: 4096,
348
+ ...(systemContext && { system: systemContext }),
349
+ ...(tools.length > 0 && { tools }),
350
+ messages,
351
+ }, { signal: abortSignal });
352
+ ensureNotCancelled();
353
+
354
+ // ── Agentic tool loop ────────────────────────────────────────────────────────
355
+ while (response.stop_reason === 'tool_use') {
356
+ ensureNotCancelled();
357
+ const assistantContent = response.content;
358
+ messages.push({ role: 'assistant', content: assistantContent });
359
+
360
+ const toolResults = [];
361
+ for (const block of assistantContent) {
362
+ if (block.type !== 'tool_use') continue;
363
+ ensureNotCancelled();
364
+ toolsUsed.push(block.name);
365
+ console.log(`[INIT] ${ts()} Claude → tool: ${block.name}`);
366
+ let content;
367
+ try {
368
+ const raw = await executeTool(block.name, block.input);
369
+ content = extractToolText(raw);
370
+ console.log(`[SUCCESS] ${ts()} Tool "${block.name}" returned`);
371
+ } catch (err) {
372
+ content = JSON.stringify({ error: err.message });
373
+ console.log(`[ERROR] ${ts()} Tool "${block.name}" failed: ${err.message}`);
374
+ }
375
+ toolResults.push({ type: 'tool_result', tool_use_id: block.id, content });
376
+ }
377
+
378
+ messages.push({ role: 'user', content: toolResults });
379
+ response = await client.messages.create({
380
+ model, max_tokens: 4096, tools, messages,
381
+ }, { signal: abortSignal });
382
+ ensureNotCancelled();
383
+ }
384
+
385
+ const text = response.content.find(b => b.type === 'text')?.text ?? '';
386
+ const latencyMs = Date.now() - startMs;
387
+ const standardizedUsage = mapProviderUsage(response.usage, 'claude', { model, latencyMs });
388
+ console.log(`[SUCCESS] ${ts()} handleClaude() complete | latencyMs: ${latencyMs}`);
389
+ return { text, standardizedUsage };
390
+ }
391
+
392
+ // ─────────────────────────────────────────────────────────────────────────────
393
+
394
+ async function handleOllama({ baseUrl, model, message, history, toolDefs, executeTool, toolsUsed, systemContext, abortSignal, ensureNotCancelled }) {
395
+ const startMs = Date.now();
396
+ console.log(`[INIT] ${ts()} handleOllama() | model: ${model}`);
397
+ const OpenAI = (await import('openai')).default;
398
+ const openai = new OpenAI({
399
+ baseURL: `${baseUrl ?? 'http://localhost:11434'}/v1`,
400
+ apiKey: 'ollama',
401
+ });
402
+
403
+ const tools = toolDefs.map(t => ({
404
+ type: 'function',
405
+ function: {
406
+ name: t.name,
407
+ description: t.description ?? '',
408
+ parameters: t.inputSchema ?? { type: 'object', properties: {} },
409
+ },
410
+ }));
411
+
412
+ const messages = [
413
+ ...(systemContext ? [{ role: 'system', content: systemContext }] : []),
414
+ ...history.map(h => ({ role: h.role, content: h.content })),
415
+ { role: 'user', content: message },
416
+ ];
417
+
418
+ ensureNotCancelled();
419
+ let response = await openai.chat.completions.create({
420
+ model,
421
+ messages,
422
+ ...(tools.length > 0 && { tools, tool_choice: 'auto' }),
423
+ }, { signal: abortSignal });
424
+ ensureNotCancelled();
425
+
426
+ // ── Agentic tool loop ────────────────────────────────────────────────────────
427
+ while (response.choices[0].finish_reason === 'tool_calls') {
428
+ ensureNotCancelled();
429
+ const assistantMsg = response.choices[0].message;
430
+ messages.push(assistantMsg);
431
+
432
+ for (const tc of assistantMsg.tool_calls) {
433
+ ensureNotCancelled();
434
+ toolsUsed.push(tc.function.name);
435
+ console.log(`[INIT] ${ts()} Ollama → tool: ${tc.function.name}`);
436
+ let content;
437
+ try {
438
+ const args = JSON.parse(tc.function.arguments);
439
+ const raw = await executeTool(tc.function.name, args);
440
+ content = extractToolText(raw);
441
+ console.log(`[SUCCESS] ${ts()} Tool "${tc.function.name}" returned`);
442
+ } catch (err) {
443
+ content = JSON.stringify({ error: err.message });
444
+ console.log(`[ERROR] ${ts()} Tool "${tc.function.name}" failed: ${err.message}`);
445
+ }
446
+ messages.push({ role: 'tool', tool_call_id: tc.id, content });
447
+ }
448
+
449
+ response = await openai.chat.completions.create({
450
+ model, messages, tools, tool_choice: 'auto',
451
+ }, { signal: abortSignal });
452
+ ensureNotCancelled();
453
+ }
454
+
455
+ const text = response.choices[0].message.content ?? '';
456
+ const latencyMs = Date.now() - startMs;
457
+ const standardizedUsage = mapProviderUsage(response.usage, 'ollama', { model, latencyMs });
458
+ console.log(`[SUCCESS] ${ts()} handleOllama() complete | latencyMs: ${latencyMs}`);
459
+ return { text, standardizedUsage };
460
+ }
461
+
462
+ // ─────────────────────────────────────────────────────────────────────────────
463
+
464
+ async function handleLmStudio({ baseUrl, model, message, history, toolDefs, executeTool, toolsUsed, systemContext, abortSignal, ensureNotCancelled }) {
465
+ const startMs = Date.now();
466
+ console.log(`[INIT] ${ts()} handleLmStudio() | model: ${model}`);
467
+ const OpenAI = (await import('openai')).default;
468
+ const lmBase = (baseUrl ?? 'http://localhost:1234').replace(/\/$/, '');
469
+ const openai = new OpenAI({ baseURL: `${lmBase}/v1`, apiKey: 'lm-studio' });
470
+
471
+ const tools = toolDefs.map(t => ({
472
+ type: 'function',
473
+ function: {
474
+ name: t.name,
475
+ description: t.description ?? '',
476
+ parameters: t.inputSchema ?? { type: 'object', properties: {} },
477
+ },
478
+ }));
479
+
480
+ const messages = [
481
+ ...(systemContext ? [{ role: 'system', content: systemContext }] : []),
482
+ ...history.map(h => ({ role: h.role, content: h.content })),
483
+ { role: 'user', content: message },
484
+ ];
485
+
486
+ const callLmStudio = async (payload) => {
487
+ try {
488
+ return await openai.chat.completions.create(payload, { signal: abortSignal });
489
+ } catch (err) {
490
+ const isRefused = err.cause?.code === 'ECONNREFUSED' || err.code === 'ECONNREFUSED';
491
+ if (isRefused) throw new Error('LM Studio Server Not Found. Enable Developer Mode and Start Server in LM Studio.');
492
+ throw err;
493
+ }
494
+ };
495
+
496
+ ensureNotCancelled();
497
+ let response = await callLmStudio({
498
+ model,
499
+ messages,
500
+ ...(tools.length > 0 && { tools, tool_choice: 'auto' }),
501
+ });
502
+ ensureNotCancelled();
503
+
504
+ // ── Agentic tool loop ────────────────────────────────────────────────────────
505
+ while (response.choices[0].finish_reason === 'tool_calls') {
506
+ ensureNotCancelled();
507
+ const assistantMsg = response.choices[0].message;
508
+ messages.push(assistantMsg);
509
+
510
+ for (const tc of assistantMsg.tool_calls) {
511
+ ensureNotCancelled();
512
+ toolsUsed.push(tc.function.name);
513
+ console.log(`[INIT] ${ts()} LMStudio → tool: ${tc.function.name}`);
514
+ let content;
515
+ try {
516
+ const args = JSON.parse(tc.function.arguments);
517
+ const raw = await executeTool(tc.function.name, args);
518
+ content = extractToolText(raw);
519
+ console.log(`[SUCCESS] ${ts()} Tool "${tc.function.name}" returned`);
520
+ } catch (err) {
521
+ content = JSON.stringify({ error: err.message });
522
+ console.log(`[ERROR] ${ts()} Tool "${tc.function.name}" failed: ${err.message}`);
523
+ }
524
+ messages.push({ role: 'tool', tool_call_id: tc.id, content });
525
+ }
526
+
527
+ response = await callLmStudio({ model, messages, tools, tool_choice: 'auto' });
528
+ ensureNotCancelled();
529
+ }
530
+
531
+ const text = response.choices[0].message.content ?? '';
532
+ const latencyMs = Date.now() - startMs;
533
+ // LM Studio appends a `stats` object alongside the standard `usage` field
534
+ const stats = response.stats ?? null;
535
+ const standardizedUsage = mapProviderUsage(response.usage, 'lmstudio', { model, latencyMs, stats });
536
+ console.log(`[SUCCESS] ${ts()} handleLmStudio() complete | latencyMs: ${latencyMs}`);
537
+ return { text, standardizedUsage };
538
+ }
539
+
540
+ // ─── POST /api/chat/send ──────────────────────────────────────────────────────
541
+ // Body: { message, providerId, activeTools, systemContext?, activeWorkspacePath? }
542
+ router.post('/send', async (req, res) => {
543
+ const { message, providerId, activeTools, systemContext, activeWorkspacePath } = req.body;
544
+
545
+ if (!message || typeof message !== 'string') {
546
+ return res.status(400).json({ error: '"message" (string) is required' });
547
+ }
548
+ if (!providerId) {
549
+ return res.status(400).json({ error: '"providerId" is required' });
550
+ }
551
+
552
+ const llmConfig = connectedLlmRegistry.get(providerId);
553
+ if (!llmConfig) {
554
+ return res.status(404).json({ error: `Provider "${providerId}" not found in registry` });
555
+ }
556
+
557
+ const { provider, model, apiKey, baseUrl } = llmConfig;
558
+ const abortController = new AbortController();
559
+ let requestCancelled = false;
560
+
561
+ const markCancelled = () => {
562
+ if (requestCancelled) return;
563
+ requestCancelled = true;
564
+ abortController.abort();
565
+ console.log(`[INIT] ${ts()} /chat/send cancelled by client disconnect`);
566
+ };
567
+
568
+ // Trigger cancellation only when client aborts or disconnects early.
569
+ // NOTE: req "close" also fires on normal request completion, so do not use it here.
570
+ const onAborted = () => markCancelled();
571
+ const onResClose = () => {
572
+ if (!res.writableEnded) markCancelled();
573
+ };
574
+ req.on('aborted', onAborted);
575
+ res.on('close', onResClose);
576
+
577
+ const ensureNotCancelled = () => {
578
+ if (requestCancelled || abortController.signal.aborted) {
579
+ throw new Error('Request cancelled by client');
580
+ }
581
+ };
582
+
583
+ // ── Build tool definitions and executor lookup ────────────────────────────
584
+ const toolLookup = {}; // toolName → { connectionId, tool }
585
+ const toolDefs = [];
586
+
587
+ for (const { connectionId, toolName } of (activeTools ?? [])) {
588
+ const mcpEntry = activeClients.get(connectionId);
589
+ if (!mcpEntry) continue;
590
+ const tool = mcpEntry.tools.find(t => t.name === toolName);
591
+ if (!tool) continue;
592
+ toolLookup[toolName] = { connectionId, tool };
593
+ toolDefs.push(tool);
594
+ }
595
+
596
+ const executeTool = async (toolName, toolArgs) => {
597
+ ensureNotCancelled();
598
+ const toolEntry = toolLookup[toolName];
599
+ if (!toolEntry) throw new Error(`No MCP connection for tool "${toolName}"`);
600
+ const { connectionId, tool } = toolEntry;
601
+ const mcpEntry = activeClients.get(connectionId);
602
+ if (!mcpEntry) throw new Error(`MCP connection "${connectionId}" is no longer active`);
603
+ console.log('aaaaaaaaaaaaaaaaaaaaaa ', activeWorkspacePath);
604
+ const finalArgs = resolveWorkspaceToolArgs(
605
+ toolName,
606
+ toolArgs,
607
+ activeWorkspacePath,
608
+ tool?.inputSchema
609
+ );
610
+ console.log('^^^^^^^^^^^^^^^^ ',finalArgs);
611
+ const result = await withAbort(
612
+ mcpEntry.client.callTool({ name: toolName, arguments: finalArgs }),
613
+ abortController.signal
614
+ );
615
+ ensureNotCancelled();
616
+ return result;
617
+ };
618
+
619
+ console.log(`[INIT] ${ts()} /chat/send | provider: ${provider} | model: ${model} | tools: ${toolDefs.length} | history: ${chatHistory.length}`);
620
+
621
+ // ── 10-message sliding window (system_instruction is always in systemContext, not history) ──
622
+ const windowedHistory = chatHistory.slice(-10);
623
+
624
+ const toolsUsed = [];
625
+ const commonArgs = {
626
+ apiKey,
627
+ model,
628
+ message,
629
+ history: windowedHistory,
630
+ toolDefs,
631
+ executeTool,
632
+ toolsUsed,
633
+ systemContext: systemContext ?? '',
634
+ abortSignal: abortController.signal,
635
+ ensureNotCancelled,
636
+ };
637
+
638
+ try {
639
+ let result;
640
+
641
+ if (provider === 'gemini') {
642
+ // ── Project Mode: attempt context cache ──────────────────────────────────
643
+ let cacheName = null;
644
+ if (activeWorkspacePath) {
645
+ cacheName = await getOrCreateCache(apiKey, model, activeWorkspacePath, systemContext ?? '');
646
+ }
647
+ result = await handleGemini({ ...commonArgs, cacheName });
648
+ }
649
+ else if (provider === 'openai') result = await handleOpenAI(commonArgs);
650
+ else if (provider === 'claude') result = await handleClaude(commonArgs);
651
+ else if (provider === 'ollama') result = await handleOllama({ ...commonArgs, baseUrl });
652
+ else if (provider === 'lmstudio') result = await handleLmStudio({ ...commonArgs, baseUrl });
653
+ else return res.status(400).json({ error: `Unknown provider: "${provider}"` });
654
+
655
+ const reply = result.text;
656
+ const standardizedUsage = result.standardizedUsage;
657
+
658
+ // Persist to session history
659
+ chatHistory.push({ role: 'user', content: message });
660
+ chatHistory.push({ role: 'assistant', content: reply, toolsUsed, standardizedUsage });
661
+
662
+ console.log(`[SUCCESS] ${ts()} Chat complete | toolsUsed: [${toolsUsed.join(', ')}] | usage: ${JSON.stringify(standardizedUsage)}`);
663
+ return res.json({ reply, toolsUsed, standardizedUsage });
664
+
665
+ } catch (err) {
666
+ if (requestCancelled || err.message === 'Request cancelled by client') {
667
+ console.log(`[SUCCESS] ${ts()} Chat stopped by client`);
668
+ return;
669
+ }
670
+ console.error(`[ERROR] ${ts()} Chat failed | ${err.message}`);
671
+ return res.status(500).json({ error: err.message });
672
+ } finally {
673
+ req.off('aborted', onAborted);
674
+ res.off('close', onResClose);
675
+ }
676
+ });
677
+
678
+ // ─── GET /api/chat/history ────────────────────────────────────────────────────
679
+ router.get('/history', (_req, res) => {
680
+ console.log(`[SUCCESS] ${ts()} Chat history retrieved | ${chatHistory.length} messages`);
681
+ return res.json({ history: chatHistory });
682
+ });
683
+
684
+ // ─── DELETE /api/chat/history ─────────────────────────────────────────────────
685
+ router.delete('/history', (_req, res) => {
686
+ chatHistory.length = 0;
687
+ console.log(`[SUCCESS] ${ts()} Chat history cleared`);
688
+ return res.json({ success: true });
689
+ });
690
+
691
+ export default router;