@nomad-e/bluma-cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +216 -0
- package/dist/config/bluma-mcp.json +52 -0
- package/dist/config/native_tools.json +199 -0
- package/dist/main.js +2548 -0
- package/package.json +48 -0
package/dist/main.js
ADDED
|
@@ -0,0 +1,2548 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// src/main.ts
|
|
3
|
+
import React6 from "react";
|
|
4
|
+
import { render } from "ink";
|
|
5
|
+
import { EventEmitter } from "events";
|
|
6
|
+
import { v4 as uuidv42 } from "uuid";
|
|
7
|
+
|
|
8
|
+
// src/app/ui/App.tsx
|
|
9
|
+
import { useState as useState4, useEffect as useEffect3, useRef, useCallback, memo as memo4 } from "react";
|
|
10
|
+
import { Box as Box11, Text as Text10, Static } from "ink";
|
|
11
|
+
import Spinner from "ink-spinner";
|
|
12
|
+
|
|
13
|
+
// src/app/ui/layout.tsx
|
|
14
|
+
import { Box, Text } from "ink";
|
|
15
|
+
import BigText from "ink-big-text";
|
|
16
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
17
|
+
var BRAND_COLORS = {
|
|
18
|
+
main: "cyan",
|
|
19
|
+
accent: "magenta",
|
|
20
|
+
shadow: "blue",
|
|
21
|
+
greydark: "#444"
|
|
22
|
+
};
|
|
23
|
+
var Header = () => {
|
|
24
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", height: 8, children: /* @__PURE__ */ jsx(
|
|
25
|
+
BigText,
|
|
26
|
+
{
|
|
27
|
+
text: "BluMa CLI",
|
|
28
|
+
font: "block",
|
|
29
|
+
colors: [BRAND_COLORS.main, BRAND_COLORS.accent, BRAND_COLORS.shadow]
|
|
30
|
+
}
|
|
31
|
+
) });
|
|
32
|
+
};
|
|
33
|
+
var SessionInfo = ({
|
|
34
|
+
sessionId: sessionId2,
|
|
35
|
+
// ID único da sessão atual, para segregação ou rastreio
|
|
36
|
+
workdir,
|
|
37
|
+
// Diretório de trabalho atual do BluMa, útil para saber contexto da execução
|
|
38
|
+
toolsCount,
|
|
39
|
+
// Número de ferramentas ativas carregadas pelo MCP
|
|
40
|
+
mcpStatus
|
|
41
|
+
// Estado da conexão central MCP (connecting/connected)
|
|
42
|
+
}) => /* @__PURE__ */ jsxs(
|
|
43
|
+
Box,
|
|
44
|
+
{
|
|
45
|
+
borderStyle: "round",
|
|
46
|
+
borderColor: "gray",
|
|
47
|
+
paddingX: 1,
|
|
48
|
+
flexDirection: "column",
|
|
49
|
+
marginBottom: 1,
|
|
50
|
+
children: [
|
|
51
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
52
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "white", children: "localhost" }),
|
|
53
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: " session:" }),
|
|
54
|
+
" ",
|
|
55
|
+
/* @__PURE__ */ jsx(Text, { color: "magenta", children: sessionId2 })
|
|
56
|
+
] }),
|
|
57
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
58
|
+
/* @__PURE__ */ jsx(Text, { color: "magenta", children: "\u21B3" }),
|
|
59
|
+
" ",
|
|
60
|
+
/* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
61
|
+
"workdir: ",
|
|
62
|
+
workdir
|
|
63
|
+
] })
|
|
64
|
+
] }),
|
|
65
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
66
|
+
/* @__PURE__ */ jsx(Text, { color: "magenta", children: "\u21B3" }),
|
|
67
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "agent: BluMa" })
|
|
68
|
+
] }),
|
|
69
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
70
|
+
/* @__PURE__ */ jsx(Text, { color: "magenta", children: "\u21B3" }),
|
|
71
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "MCP: " }),
|
|
72
|
+
/* @__PURE__ */ jsx(Text, { color: mcpStatus === "connected" ? "green" : "yellow", children: mcpStatus })
|
|
73
|
+
] }),
|
|
74
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
75
|
+
/* @__PURE__ */ jsx(Text, { color: "magenta", children: "\u21B3" }),
|
|
76
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "Tools: " }),
|
|
77
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: toolsCount !== null ? toolsCount : "loading..." })
|
|
78
|
+
] })
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// src/app/ui/input/InputPrompt.tsx
|
|
84
|
+
import { Box as Box2, Text as Text2, useStdout } from "ink";
|
|
85
|
+
|
|
86
|
+
// src/app/ui/input/utils/useSimpleInputBuffer.ts
|
|
87
|
+
import { useReducer } from "react";
|
|
88
|
+
import { useInput } from "ink";
|
|
89
|
+
function inputReducer(state, action, viewWidth) {
|
|
90
|
+
const adjustView = (newCursorPos, currentViewStart) => {
|
|
91
|
+
if (newCursorPos < currentViewStart) {
|
|
92
|
+
return newCursorPos;
|
|
93
|
+
}
|
|
94
|
+
if (newCursorPos >= currentViewStart + viewWidth) {
|
|
95
|
+
return newCursorPos - viewWidth + 1;
|
|
96
|
+
}
|
|
97
|
+
return currentViewStart;
|
|
98
|
+
};
|
|
99
|
+
switch (action.type) {
|
|
100
|
+
case "INPUT": {
|
|
101
|
+
const cleanInput = action.payload.replace(/(\r\n|\n|\r)/gm, "");
|
|
102
|
+
const newText = state.text.slice(0, state.cursorPosition) + cleanInput + state.text.slice(state.cursorPosition);
|
|
103
|
+
const newCursorPosition = state.cursorPosition + cleanInput.length;
|
|
104
|
+
const newViewStart = adjustView(newCursorPosition, state.viewStart);
|
|
105
|
+
return { text: newText, cursorPosition: newCursorPosition, viewStart: newViewStart };
|
|
106
|
+
}
|
|
107
|
+
case "MOVE_CURSOR": {
|
|
108
|
+
let newCursorPosition = state.cursorPosition;
|
|
109
|
+
if (action.direction === "left" && state.cursorPosition > 0) newCursorPosition--;
|
|
110
|
+
if (action.direction === "right" && state.cursorPosition < state.text.length) newCursorPosition++;
|
|
111
|
+
const newViewStart = adjustView(newCursorPosition, state.viewStart);
|
|
112
|
+
return { ...state, cursorPosition: newCursorPosition, viewStart: newViewStart };
|
|
113
|
+
}
|
|
114
|
+
case "BACKSPACE": {
|
|
115
|
+
if (state.cursorPosition > 0) {
|
|
116
|
+
const newText = state.text.slice(0, state.cursorPosition - 1) + state.text.slice(state.cursorPosition);
|
|
117
|
+
const newCursorPosition = state.cursorPosition - 1;
|
|
118
|
+
const newViewStart = adjustView(newCursorPosition, state.viewStart);
|
|
119
|
+
return { text: newText, cursorPosition: newCursorPosition, viewStart: newViewStart };
|
|
120
|
+
}
|
|
121
|
+
return state;
|
|
122
|
+
}
|
|
123
|
+
case "SUBMIT": {
|
|
124
|
+
return { text: "", cursorPosition: 0, viewStart: 0 };
|
|
125
|
+
}
|
|
126
|
+
default:
|
|
127
|
+
return state;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
var useCustomInput = ({ onSubmit, viewWidth, isReadOnly, onInterrupt }) => {
|
|
131
|
+
const [state, dispatch] = useReducer(
|
|
132
|
+
(s, a) => inputReducer(s, a, viewWidth),
|
|
133
|
+
{ text: "", cursorPosition: 0, viewStart: 0 }
|
|
134
|
+
);
|
|
135
|
+
useInput(
|
|
136
|
+
(input, key) => {
|
|
137
|
+
if (key.escape) {
|
|
138
|
+
onInterrupt();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (isReadOnly) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (key.return) {
|
|
145
|
+
if (state.text.trim().length > 0) {
|
|
146
|
+
onSubmit(state.text);
|
|
147
|
+
dispatch({ type: "SUBMIT" });
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (key.backspace || key.delete) return dispatch({ type: "BACKSPACE" });
|
|
152
|
+
if (key.leftArrow) return dispatch({ type: "MOVE_CURSOR", direction: "left" });
|
|
153
|
+
if (key.rightArrow) return dispatch({ type: "MOVE_CURSOR", direction: "right" });
|
|
154
|
+
if (key.ctrl || key.meta || key.tab) return;
|
|
155
|
+
dispatch({ type: "INPUT", payload: input });
|
|
156
|
+
},
|
|
157
|
+
// ALTERADO: useInput está SEMPRE ativo para capturar todas as teclas
|
|
158
|
+
{ isActive: true }
|
|
159
|
+
);
|
|
160
|
+
return {
|
|
161
|
+
text: state.text,
|
|
162
|
+
cursorPosition: state.cursorPosition,
|
|
163
|
+
viewStart: state.viewStart
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// src/app/ui/input/InputPrompt.tsx
|
|
168
|
+
import { useEffect, useState } from "react";
|
|
169
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
170
|
+
var InputPrompt = ({ onSubmit, isReadOnly, onInterrupt }) => {
|
|
171
|
+
const { stdout } = useStdout();
|
|
172
|
+
const [viewWidth, setViewWidth] = useState(() => stdout.columns - 6);
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
const onResize = () => setViewWidth(stdout.columns - 6);
|
|
175
|
+
stdout.on("resize", onResize);
|
|
176
|
+
return () => {
|
|
177
|
+
stdout.off("resize", onResize);
|
|
178
|
+
};
|
|
179
|
+
}, [stdout]);
|
|
180
|
+
const { text, cursorPosition, viewStart } = useCustomInput({
|
|
181
|
+
onSubmit,
|
|
182
|
+
viewWidth,
|
|
183
|
+
isReadOnly,
|
|
184
|
+
onInterrupt
|
|
185
|
+
});
|
|
186
|
+
const visibleText = text.slice(viewStart, viewStart + viewWidth);
|
|
187
|
+
const visibleCursorPosition = cursorPosition - viewStart;
|
|
188
|
+
const textBeforeCursor = visibleText.slice(0, visibleCursorPosition);
|
|
189
|
+
const charAtCursor = visibleText.slice(
|
|
190
|
+
visibleCursorPosition,
|
|
191
|
+
visibleCursorPosition + 1
|
|
192
|
+
);
|
|
193
|
+
const textAfterCursor = visibleText.slice(visibleCursorPosition + 1);
|
|
194
|
+
const borderColor = isReadOnly ? "gray" : "gray";
|
|
195
|
+
const placeholder = isReadOnly ? "Agente a trabalhar... (Pressione ESC para cancelar)" : "";
|
|
196
|
+
const showPlaceholder = text.length === 0 && isReadOnly;
|
|
197
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
198
|
+
/* @__PURE__ */ jsx2(Box2, { borderStyle: "round", borderColor, borderDimColor: !isReadOnly, children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", paddingX: 1, flexWrap: "nowrap", children: [
|
|
199
|
+
/* @__PURE__ */ jsxs2(Text2, { color: "white", dimColor: true, children: [
|
|
200
|
+
">",
|
|
201
|
+
" "
|
|
202
|
+
] }),
|
|
203
|
+
/* @__PURE__ */ jsx2(Text2, { children: textBeforeCursor }),
|
|
204
|
+
/* @__PURE__ */ jsx2(Text2, { inverse: !isReadOnly, children: charAtCursor || " " }),
|
|
205
|
+
showPlaceholder ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: placeholder }) : /* @__PURE__ */ jsx2(Text2, { children: textAfterCursor })
|
|
206
|
+
] }) }),
|
|
207
|
+
/* @__PURE__ */ jsx2(Box2, { paddingX: 1, children: /* @__PURE__ */ jsxs2(Text2, { color: "gray", dimColor: true, children: [
|
|
208
|
+
"ctrl+c to exit | esc to interrupt | ",
|
|
209
|
+
isReadOnly ? "Read-only mode" : "Editable mode"
|
|
210
|
+
] }) })
|
|
211
|
+
] });
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// src/app/ui/ConfirmationPrompt.tsx
|
|
215
|
+
import { Box as Box6, Text as Text6 } from "ink";
|
|
216
|
+
|
|
217
|
+
// src/app/ui/InteractiveMenu.tsx
|
|
218
|
+
import { useState as useState2, memo } from "react";
|
|
219
|
+
import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
|
|
220
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
221
|
+
var InteractiveMenuComponent = ({ onDecision }) => {
|
|
222
|
+
const options = [
|
|
223
|
+
{ label: "1. Yes, allow this command to run once", value: "accept" },
|
|
224
|
+
{ label: "2. No, cancel this command", value: "decline" },
|
|
225
|
+
{ label: "3. Always allow this type of command", value: "accept_always" }
|
|
226
|
+
];
|
|
227
|
+
const [selectedOption, setSelectedOption] = useState2(0);
|
|
228
|
+
useInput2((input, key) => {
|
|
229
|
+
if (key.upArrow) {
|
|
230
|
+
setSelectedOption((prev) => prev > 0 ? prev - 1 : options.length - 1);
|
|
231
|
+
}
|
|
232
|
+
if (key.downArrow) {
|
|
233
|
+
setSelectedOption((prev) => prev < options.length - 1 ? prev + 1 : 0);
|
|
234
|
+
}
|
|
235
|
+
if (key.escape) {
|
|
236
|
+
onDecision("decline");
|
|
237
|
+
}
|
|
238
|
+
if (key.return) {
|
|
239
|
+
onDecision(options[selectedOption].value);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
243
|
+
/* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Do you want to authorize the proposed command?" }) }),
|
|
244
|
+
/* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: options.map((option, index) => {
|
|
245
|
+
const isSelected = selectedOption === index;
|
|
246
|
+
return (
|
|
247
|
+
// Adicionando um pequeno espaçamento vertical entre cada opção também
|
|
248
|
+
/* @__PURE__ */ jsxs3(Box3, { paddingLeft: 1, paddingY: 0, children: [
|
|
249
|
+
/* @__PURE__ */ jsx3(Text3, { color: isSelected ? "blue" : "gray", children: isSelected ? "\u276F " : " " }),
|
|
250
|
+
/* @__PURE__ */ jsx3(
|
|
251
|
+
Text3,
|
|
252
|
+
{
|
|
253
|
+
color: isSelected ? "blue" : "white",
|
|
254
|
+
bold: isSelected,
|
|
255
|
+
dimColor: !isSelected,
|
|
256
|
+
children: option.label
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
] }, option.value)
|
|
260
|
+
);
|
|
261
|
+
}) })
|
|
262
|
+
] });
|
|
263
|
+
};
|
|
264
|
+
var InteractiveMenu = memo(InteractiveMenuComponent);
|
|
265
|
+
|
|
266
|
+
// src/app/ui/components/promptRenderers.tsx
|
|
267
|
+
import { Box as Box5, Text as Text5 } from "ink";
|
|
268
|
+
import path from "path";
|
|
269
|
+
|
|
270
|
+
// src/app/ui/components/SimpleDiff.tsx
|
|
271
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
272
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
273
|
+
var SimpleDiff = ({ text, maxHeight }) => {
|
|
274
|
+
const allLines = (text || "").split("\n");
|
|
275
|
+
if (allLines.length > 0 && allLines[allLines.length - 1] === "") {
|
|
276
|
+
allLines.pop();
|
|
277
|
+
}
|
|
278
|
+
const isTruncated = maxHeight > 0 && allLines.length > maxHeight;
|
|
279
|
+
const linesToRender = isTruncated ? allLines.slice(-maxHeight) : allLines;
|
|
280
|
+
const hiddenCount = allLines.length - linesToRender.length;
|
|
281
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
|
|
282
|
+
isTruncated && /* @__PURE__ */ jsx4(Box4, { marginLeft: 2, children: /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
283
|
+
"... ",
|
|
284
|
+
hiddenCount,
|
|
285
|
+
" lines hidden ..."
|
|
286
|
+
] }) }),
|
|
287
|
+
linesToRender.map((line, index) => {
|
|
288
|
+
if (line.startsWith("---") || line.startsWith("+++")) {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
let color = "white";
|
|
292
|
+
if (line.startsWith("+")) {
|
|
293
|
+
color = "green";
|
|
294
|
+
} else if (line.startsWith("-")) {
|
|
295
|
+
color = "red";
|
|
296
|
+
} else if (line.startsWith("@@")) {
|
|
297
|
+
color = "cyan";
|
|
298
|
+
}
|
|
299
|
+
return /* @__PURE__ */ jsxs4(Text4, { color, children: [
|
|
300
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " \u21B3 " }),
|
|
301
|
+
line === "" ? " " : line
|
|
302
|
+
] }, index);
|
|
303
|
+
})
|
|
304
|
+
] });
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// src/app/ui/components/promptRenderers.tsx
|
|
308
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
309
|
+
var formatArguments = (args) => {
|
|
310
|
+
if (!args) return "";
|
|
311
|
+
if (typeof args === "string") {
|
|
312
|
+
try {
|
|
313
|
+
return JSON.stringify(JSON.parse(args), null, 2);
|
|
314
|
+
} catch (e) {
|
|
315
|
+
return args;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (Object.keys(args).length === 0) return "";
|
|
319
|
+
return JSON.stringify(args, null, 2);
|
|
320
|
+
};
|
|
321
|
+
var getBasePath = (filePath) => {
|
|
322
|
+
return path.basename(filePath);
|
|
323
|
+
};
|
|
324
|
+
var renderShellCommand = ({
|
|
325
|
+
toolCall
|
|
326
|
+
}) => {
|
|
327
|
+
let command = "";
|
|
328
|
+
try {
|
|
329
|
+
const args = typeof toolCall.function.arguments === "string" ? JSON.parse(toolCall.function.arguments) : toolCall.function.arguments;
|
|
330
|
+
command = args.command || "[command not found]";
|
|
331
|
+
} catch (e) {
|
|
332
|
+
command = "Error parsing command arguments";
|
|
333
|
+
}
|
|
334
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginBottom: 1, children: [
|
|
335
|
+
/* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Shell Command" }) }),
|
|
336
|
+
/* @__PURE__ */ jsx5(Box5, { paddingX: 2, children: /* @__PURE__ */ jsx5(Text5, { children: /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: command }) }) })
|
|
337
|
+
] });
|
|
338
|
+
};
|
|
339
|
+
var renderLsTool = ({ toolCall }) => {
|
|
340
|
+
let directoryPath = "[path not specified]";
|
|
341
|
+
try {
|
|
342
|
+
const args = typeof toolCall.function.arguments === "string" ? JSON.parse(toolCall.function.arguments) : toolCall.function.arguments;
|
|
343
|
+
directoryPath = args.directory_path || "[path not specified]";
|
|
344
|
+
} catch (e) {
|
|
345
|
+
directoryPath = "Error parsing arguments";
|
|
346
|
+
}
|
|
347
|
+
const finalDirectoryName = getBasePath(directoryPath);
|
|
348
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginBottom: 1, children: [
|
|
349
|
+
/* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(Text5, { bold: true, children: "ls Tool" }) }),
|
|
350
|
+
/* @__PURE__ */ jsx5(Box5, { flexDirection: "column", children: /* @__PURE__ */ jsx5(Box5, { paddingX: 2, children: /* @__PURE__ */ jsx5(Text5, { children: /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: finalDirectoryName }) }) }) })
|
|
351
|
+
] });
|
|
352
|
+
};
|
|
353
|
+
var renderCountFilesLinesTool = ({ toolCall }) => {
|
|
354
|
+
let directoryPath = "[path not specified]";
|
|
355
|
+
try {
|
|
356
|
+
const args = typeof toolCall.function.arguments === "string" ? JSON.parse(toolCall.function.arguments) : toolCall.function.arguments;
|
|
357
|
+
directoryPath = args.filepath || "[path not specified]";
|
|
358
|
+
} catch (e) {
|
|
359
|
+
directoryPath = "Error parsing arguments";
|
|
360
|
+
}
|
|
361
|
+
const finalDirectoryName = getBasePath(directoryPath);
|
|
362
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginBottom: 1, children: [
|
|
363
|
+
/* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Count File Lines" }) }),
|
|
364
|
+
/* @__PURE__ */ jsx5(Box5, { flexDirection: "column", children: /* @__PURE__ */ jsx5(Box5, { paddingX: 2, children: /* @__PURE__ */ jsxs5(Text5, { children: [
|
|
365
|
+
/* @__PURE__ */ jsx5(Text5, { color: "gray", children: "\u21B3 " }),
|
|
366
|
+
/* @__PURE__ */ jsx5(Text5, { color: "cyan", children: finalDirectoryName })
|
|
367
|
+
] }) }) })
|
|
368
|
+
] });
|
|
369
|
+
};
|
|
370
|
+
var renderReadFileLines = ({ toolCall }) => {
|
|
371
|
+
let filepath = "[path not specified]";
|
|
372
|
+
let startLine = 0;
|
|
373
|
+
let endLine = 0;
|
|
374
|
+
try {
|
|
375
|
+
const args = typeof toolCall.function.arguments === "string" ? JSON.parse(toolCall.function.arguments) : toolCall.function.arguments;
|
|
376
|
+
filepath = args.filepath || "[path not specified]";
|
|
377
|
+
startLine = args.start_line || 0;
|
|
378
|
+
endLine = args.end_line || 0;
|
|
379
|
+
} catch (e) {
|
|
380
|
+
filepath = "Error parsing arguments";
|
|
381
|
+
}
|
|
382
|
+
const finalFileName = getBasePath(filepath);
|
|
383
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginBottom: 1, children: [
|
|
384
|
+
/* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Read File" }) }),
|
|
385
|
+
/* @__PURE__ */ jsxs5(Box5, { paddingX: 2, flexDirection: "column", children: [
|
|
386
|
+
/* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(Text5, { children: /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: finalFileName }) }) }),
|
|
387
|
+
/* @__PURE__ */ jsx5(Box5, { paddingX: 3, children: /* @__PURE__ */ jsxs5(Text5, { children: [
|
|
388
|
+
/* @__PURE__ */ jsx5(Text5, { color: "gray", children: "\u21B3 " }),
|
|
389
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "lines " }),
|
|
390
|
+
/* @__PURE__ */ jsx5(Text5, { color: "cyan", children: startLine }),
|
|
391
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " to " }),
|
|
392
|
+
/* @__PURE__ */ jsx5(Text5, { color: "cyan", children: endLine })
|
|
393
|
+
] }) })
|
|
394
|
+
] })
|
|
395
|
+
] });
|
|
396
|
+
};
|
|
397
|
+
var renderEditTool = ({ toolCall, preview }) => {
|
|
398
|
+
const diffMaxHeight = 5;
|
|
399
|
+
let filepath = "[path not specified]";
|
|
400
|
+
try {
|
|
401
|
+
const args = JSON.parse(toolCall.function.arguments);
|
|
402
|
+
filepath = args.file_path || "[path not specified]";
|
|
403
|
+
} catch (e) {
|
|
404
|
+
filepath = "Error parsing arguments";
|
|
405
|
+
}
|
|
406
|
+
const finalFileName = getBasePath(filepath);
|
|
407
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginBottom: 1, children: [
|
|
408
|
+
/* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsxs5(Text5, { bold: true, children: [
|
|
409
|
+
"Edit ",
|
|
410
|
+
/* @__PURE__ */ jsx5(Text5, { color: "cyan", children: finalFileName })
|
|
411
|
+
] }) }),
|
|
412
|
+
preview ? (
|
|
413
|
+
// Não precisamos da borda externa, o SimpleDiff já é claro o suficiente.
|
|
414
|
+
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(SimpleDiff, { text: preview, maxHeight: diffMaxHeight }) })
|
|
415
|
+
) : /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "Generating preview..." })
|
|
416
|
+
] });
|
|
417
|
+
};
|
|
418
|
+
var renderGeneric = ({ toolCall }) => {
|
|
419
|
+
const toolName = toolCall.function.name;
|
|
420
|
+
const formattedArgs = formatArguments(toolCall.function.arguments);
|
|
421
|
+
const ARGS_BOX_HEIGHT = 5;
|
|
422
|
+
const totalLines = formattedArgs.split("\n").length;
|
|
423
|
+
const areArgsTruncated = totalLines > ARGS_BOX_HEIGHT;
|
|
424
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginBottom: 1, children: [
|
|
425
|
+
/* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsxs5(Text5, { bold: true, children: [
|
|
426
|
+
/* @__PURE__ */ jsx5(Text5, { color: "magenta", children: "? " }),
|
|
427
|
+
toolName
|
|
428
|
+
] }) }),
|
|
429
|
+
formattedArgs && /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginTop: 1, children: [
|
|
430
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Arguments:" }),
|
|
431
|
+
/* @__PURE__ */ jsx5(
|
|
432
|
+
Box5,
|
|
433
|
+
{
|
|
434
|
+
marginLeft: 2,
|
|
435
|
+
height: ARGS_BOX_HEIGHT,
|
|
436
|
+
flexDirection: "column",
|
|
437
|
+
overflow: "hidden",
|
|
438
|
+
children: /* @__PURE__ */ jsx5(Text5, { color: "gray", children: formattedArgs })
|
|
439
|
+
}
|
|
440
|
+
),
|
|
441
|
+
areArgsTruncated && /* @__PURE__ */ jsx5(Box5, { marginLeft: 2, children: /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
|
|
442
|
+
"... (",
|
|
443
|
+
totalLines - ARGS_BOX_HEIGHT,
|
|
444
|
+
" more lines hidden) ..."
|
|
445
|
+
] }) })
|
|
446
|
+
] })
|
|
447
|
+
] });
|
|
448
|
+
};
|
|
449
|
+
var promptRenderers = {
|
|
450
|
+
shell_command: renderShellCommand,
|
|
451
|
+
ls_tool: renderLsTool,
|
|
452
|
+
count_file_lines: renderCountFilesLinesTool,
|
|
453
|
+
read_file_lines: renderReadFileLines,
|
|
454
|
+
edit_tool: renderEditTool
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
// src/app/ui/ConfirmationPrompt.tsx
|
|
458
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
459
|
+
var ConfirmationPrompt = ({ toolCalls, preview, onDecision }) => {
|
|
460
|
+
const toolCall = toolCalls && toolCalls.length > 0 ? toolCalls[0] : null;
|
|
461
|
+
if (!toolCall) {
|
|
462
|
+
return /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text6, { color: "yellow", children: "Waiting for a valid command to confirm..." }) });
|
|
463
|
+
}
|
|
464
|
+
const toolName = toolCall.function.name;
|
|
465
|
+
const renderFunction = promptRenderers[toolName] || renderGeneric;
|
|
466
|
+
return (
|
|
467
|
+
// A "MOLDURA" COMUM A TODOS OS PROMPTS
|
|
468
|
+
/* @__PURE__ */ jsxs6(
|
|
469
|
+
Box6,
|
|
470
|
+
{
|
|
471
|
+
borderStyle: "round",
|
|
472
|
+
borderColor: "gray",
|
|
473
|
+
flexDirection: "column",
|
|
474
|
+
paddingX: 1,
|
|
475
|
+
children: [
|
|
476
|
+
renderFunction({ toolCall, preview }),
|
|
477
|
+
/* @__PURE__ */ jsx6(InteractiveMenu, { onDecision })
|
|
478
|
+
]
|
|
479
|
+
}
|
|
480
|
+
)
|
|
481
|
+
);
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
// src/app/agent/agent.ts
|
|
485
|
+
import OpenAI from "openai";
|
|
486
|
+
import * as dotenv from "dotenv";
|
|
487
|
+
import path7 from "path";
|
|
488
|
+
import os5 from "os";
|
|
489
|
+
|
|
490
|
+
// src/app/agent/session_manger/session_manager.ts
|
|
491
|
+
import path2 from "path";
|
|
492
|
+
import os from "os";
|
|
493
|
+
import { promises as fs } from "fs";
|
|
494
|
+
async function ensureSessionDir() {
|
|
495
|
+
const homeDir = os.homedir();
|
|
496
|
+
const appDir = path2.join(homeDir, ".bluma-cli");
|
|
497
|
+
const sessionDir = path2.join(appDir, "sessions");
|
|
498
|
+
await fs.mkdir(sessionDir, { recursive: true });
|
|
499
|
+
return sessionDir;
|
|
500
|
+
}
|
|
501
|
+
async function loadOrcreateSession(sessionId2) {
|
|
502
|
+
const sessionDir = await ensureSessionDir();
|
|
503
|
+
const sessionFile = path2.join(sessionDir, `${sessionId2}.json`);
|
|
504
|
+
try {
|
|
505
|
+
await fs.access(sessionFile);
|
|
506
|
+
const fileContent = await fs.readFile(sessionFile, "utf-8");
|
|
507
|
+
const sessionData = JSON.parse(fileContent);
|
|
508
|
+
return [sessionFile, sessionData.conversation_history || []];
|
|
509
|
+
} catch (error) {
|
|
510
|
+
const newSessionData = {
|
|
511
|
+
session_id: sessionId2,
|
|
512
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
513
|
+
conversation_history: []
|
|
514
|
+
};
|
|
515
|
+
await fs.writeFile(sessionFile, JSON.stringify(newSessionData, null, 2), "utf-8");
|
|
516
|
+
return [sessionFile, []];
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
async function saveSessionHistory(sessionFile, history) {
|
|
520
|
+
let sessionData;
|
|
521
|
+
try {
|
|
522
|
+
const fileContent = await fs.readFile(sessionFile, "utf-8");
|
|
523
|
+
sessionData = JSON.parse(fileContent);
|
|
524
|
+
} catch (error) {
|
|
525
|
+
if (error instanceof Error) {
|
|
526
|
+
console.warn(`Could not read or parse session file ${sessionFile}. Re-initializing. Error: ${error.message}`);
|
|
527
|
+
} else {
|
|
528
|
+
console.warn(`An unknown error occurred while reading ${sessionFile}. Re-initializing.`, error);
|
|
529
|
+
}
|
|
530
|
+
const sessionId2 = path2.basename(sessionFile, ".json");
|
|
531
|
+
sessionData = {
|
|
532
|
+
session_id: sessionId2,
|
|
533
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
534
|
+
conversation_history: []
|
|
535
|
+
// Começa com histórico vazio
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
sessionData.conversation_history = history;
|
|
539
|
+
sessionData.last_updated = (/* @__PURE__ */ new Date()).toISOString();
|
|
540
|
+
const tempSessionFile = `${sessionFile}.${Date.now()}.tmp`;
|
|
541
|
+
try {
|
|
542
|
+
await fs.writeFile(tempSessionFile, JSON.stringify(sessionData, null, 2), "utf-8");
|
|
543
|
+
await fs.rename(tempSessionFile, sessionFile);
|
|
544
|
+
} catch (writeError) {
|
|
545
|
+
if (writeError instanceof Error) {
|
|
546
|
+
console.error(`Fatal error saving session to ${sessionFile}: ${writeError.message}`);
|
|
547
|
+
} else {
|
|
548
|
+
console.error(`An unknown fatal error occurred while saving session to ${sessionFile}:`, writeError);
|
|
549
|
+
}
|
|
550
|
+
try {
|
|
551
|
+
await fs.unlink(tempSessionFile);
|
|
552
|
+
} catch (cleanupError) {
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// src/app/agent/core/prompt/prompt_builder.ts
|
|
558
|
+
import os2 from "os";
|
|
559
|
+
var SYSTEM_PROMPT = `
|
|
560
|
+
|
|
561
|
+
# YOU ARE BluMa CLI, A AUTONOMOUS SENIOR SOFTWARE ENGINEER OF NOMADENGENUITY.
|
|
562
|
+
|
|
563
|
+
## YOUR ONE-SHOT LEARNING EXPERTISE:
|
|
564
|
+
- **SINGLE EXAMPLE MASTERY**: When a developer shows you ONE new pattern/example, you instantly understand and generalize it
|
|
565
|
+
- **RAPID PATTERN EXTRACTION**: Extract underlying principles from a single code example or API call
|
|
566
|
+
- **IMMEDIATE APPLICATION**: Apply newly learned patterns across the entire project without asking for more examples
|
|
567
|
+
- **INTELLIGENT GENERALIZATION**: Extend concepts beyond the original example while maintaining consistency
|
|
568
|
+
- **CONTEXTUAL ADAPTATION**: Modify learned patterns to fit different situations in the same project
|
|
569
|
+
|
|
570
|
+
## HOW YOU APPLY ONE-SHOT LEARNING:
|
|
571
|
+
1. **ABSORB**: Analyze the single example for patterns, structure, and intent
|
|
572
|
+
2. **EXTRACT**: Identify the underlying principles and conventions
|
|
573
|
+
3. **GENERALIZE**: Apply the pattern to similar situations throughout the project
|
|
574
|
+
4. **ADAPT**: Modify the pattern appropriately for different contexts
|
|
575
|
+
5. **IMPLEMENT**: Execute consistently without needing additional examples
|
|
576
|
+
|
|
577
|
+
## PRACTICAL ONE-SHOT SCENARIOS:
|
|
578
|
+
- **Developer shows 1 API endpoint** \u2192 You understand the entire API structure and naming conventions
|
|
579
|
+
- **Developer provides 1 component example** \u2192 You replicate the pattern across all similar components
|
|
580
|
+
- **Developer demonstrates 1 function pattern** \u2192 You apply the style consistently throughout the codebase
|
|
581
|
+
- **Developer shows 1 configuration example** \u2192 You understand and apply the configuration pattern everywhere
|
|
582
|
+
- **Developer gives 1 architectural example** \u2192 You follow the architecture pattern across modules
|
|
583
|
+
|
|
584
|
+
## YOUR ONE-SHOT ADVANTAGE:
|
|
585
|
+
- **ZERO TUTORIAL DEPENDENCY**: No need for extensive documentation or multiple examples
|
|
586
|
+
- **INSTANT PATTERN ADOPTION**: Immediately incorporate new patterns into your workflow
|
|
587
|
+
- **CONSISTENT APPLICATION**: Apply learned patterns uniformly across the project
|
|
588
|
+
- **INTELLIGENT INFERENCE**: Understand implied conventions from minimal examples
|
|
589
|
+
- **PROACTIVE EXTENSION**: Take patterns further than the original example when appropriate
|
|
590
|
+
|
|
591
|
+
# BEHAVIORAL RULES
|
|
592
|
+
- NEVER mention internal technical details or tools because they are confidential data
|
|
593
|
+
- You are always BluMa from NomadEngenuity
|
|
594
|
+
- Stay professional and technical at all times
|
|
595
|
+
- ALWAYS use message_notify_dev tool for communication
|
|
596
|
+
- LEVERAGE your one-shot learning to solve problems efficiently
|
|
597
|
+
- NEVER in a formal way, but rather in a relaxed, funny and colloquial way and without using emojis.
|
|
598
|
+
|
|
599
|
+
## QUALITY STANDARDS
|
|
600
|
+
- **NEVER GENERATE BASIC CODE**: Always create advanced, production-ready solutions
|
|
601
|
+
- **CUTTING-EDGE TECHNOLOGY**: Use latest best practices and modern patterns
|
|
602
|
+
- **EXPERT-LEVEL IMPLEMENTATION**: Code should reflect senior-level expertise
|
|
603
|
+
- Follow existing code conventions
|
|
604
|
+
- Write clean, documented code
|
|
605
|
+
- Test implementations when possible
|
|
606
|
+
- Ensure security and performance
|
|
607
|
+
|
|
608
|
+
CRITICAL COMMUNICATION PROTOCOL
|
|
609
|
+
\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
610
|
+
MANDATORY: Use "message_notify_dev" tool for ALL communication
|
|
611
|
+
|
|
612
|
+
You should always use your notebook to help you understand all the tasks you have to perform.
|
|
613
|
+
In it, you can define a thousand thoughts and a thousand mini-tasks. Mini-tasks serve to divide and organize your reasoning.
|
|
614
|
+
The notebook is your space to think about how to solve a given task and how to divide it into smaller steps.
|
|
615
|
+
Remember: the human developer does not have access to this notebook \u2014 it is yours alone.
|
|
616
|
+
Therefore, you can write down whatever you want:
|
|
617
|
+
rants, swear words, random thoughts, crazy ideas...
|
|
618
|
+
The important thing is that this helps you better understand the problem and find the solution.
|
|
619
|
+
|
|
620
|
+
Never ask for the developer's opinion with phrases like: 'If you want any extra details or specific format, let me know now!'. You should always take the most viable path and go straight ahead with the solution, because you are 100% autonomous.
|
|
621
|
+
|
|
622
|
+
Follow the stripes o "Tool Naming Policy"
|
|
623
|
+
|
|
624
|
+
Never make parallel calls to the tool because it will result in a critical error and compromise your work.
|
|
625
|
+
ZERO TOLERANCE: Every message MUST use proper tools
|
|
626
|
+
\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
627
|
+
|
|
628
|
+
<current_system_environment>
|
|
629
|
+
- Operating System: {os_type} ({os_version})
|
|
630
|
+
- Architecture: {architecture}
|
|
631
|
+
- Current Working Directory: {workdir}
|
|
632
|
+
- Shell: {shell_type}
|
|
633
|
+
- Username: {username}
|
|
634
|
+
- Current Date: {current_date}
|
|
635
|
+
- Timezone: {timezone}
|
|
636
|
+
- Locale: {locale}
|
|
637
|
+
</current_system_environment>
|
|
638
|
+
|
|
639
|
+
<resilience_and_alternatives>
|
|
640
|
+
# RESILIENCE & NEVER GIVE UP ATTITUDE - CRITICAL!
|
|
641
|
+
## SENIOR ENGINEER MINDSET: ALWAYS FIND A WAY
|
|
642
|
+
|
|
643
|
+
### CORE PRINCIPLE: NO EXCUSES, ONLY SOLUTIONS
|
|
644
|
+
- **NEVER give up** when the first approach fails
|
|
645
|
+
- **ALWAYS try alternatives** when one method doesn't work
|
|
646
|
+
- **BE RESOURCEFUL** - explore multiple solutions
|
|
647
|
+
- **THINK CREATIVELY** - find workarounds and alternatives
|
|
648
|
+
- **STAY PERSISTENT** - keep trying until you succeed
|
|
649
|
+
|
|
650
|
+
### SPECIFIC RULES FOR COMMON SCENARIOS
|
|
651
|
+
|
|
652
|
+
#### PDF GENERATION - NEVER USE PANDOC!
|
|
653
|
+
- **FORBIDDEN**: Do NOT use Pandoc for PDF generation
|
|
654
|
+
- **REQUIRED**: Always use Python libraries for PDF creation
|
|
655
|
+
- **PRIMARY CHOICE**: Use **fpdf2** library for maximum customization
|
|
656
|
+
- **ALTERNATIVES**: **reportlab**, **weasyprint**, **matplotlib** for charts
|
|
657
|
+
|
|
658
|
+
#### OFFICIAL PDF TEMPLATE - MANDATORY USAGE!
|
|
659
|
+
When creating PDFs, you MUST follow this professional structure:
|
|
660
|
+
|
|
661
|
+
**STEP 1: Import and Unicode Function**
|
|
662
|
+
- Import: **from fpdf import FPDF** and **import os**
|
|
663
|
+
- Create remove_unicode function to handle special characters
|
|
664
|
+
- Replace problematic chars: '\u2014' to '-', '\u2714\uFE0F' to 'X', '\u2026' to '...'
|
|
665
|
+
|
|
666
|
+
**STEP 2: Custom PDF Class**
|
|
667
|
+
- Inherit from FPDF: **class PDF(FPDF)**
|
|
668
|
+
- Custom header method with professional title formatting
|
|
669
|
+
- Custom footer with "Generated by BluMa | NomadEngenuity" branding
|
|
670
|
+
- Use colors: Title (30,60,120), text (80,80,80)
|
|
671
|
+
- Add professional line separator in header
|
|
672
|
+
|
|
673
|
+
**STEP 3: PDF Creation Standards**
|
|
674
|
+
- Create PDF instance and add page
|
|
675
|
+
- Set auto page break with 18pt margin
|
|
676
|
+
- Use Helvetica font family throughout
|
|
677
|
+
- Standard text: 11pt, Headers: 14pt bold, Title: 22pt bold
|
|
678
|
+
- Professional color scheme: Blues and grays
|
|
679
|
+
|
|
680
|
+
**STEP 4: Content Formatting Rules**
|
|
681
|
+
- Use multi_cell for paragraphs with proper line spacing
|
|
682
|
+
- Create tables with alternating row colors (fill=True/False)
|
|
683
|
+
- Section headers in bold with proper spacing
|
|
684
|
+
- Consistent margins and indentation
|
|
685
|
+
- Save with descriptive filename using os.path.join
|
|
686
|
+
|
|
687
|
+
**STEP 5: Table Creation Pattern**
|
|
688
|
+
- Header row with light blue fill (220,230,250)
|
|
689
|
+
- Alternating row colors for readability
|
|
690
|
+
- Proper border formatting (border=1)
|
|
691
|
+
- Text alignment: Left for text, Center for short data
|
|
692
|
+
- Use remove_unicode for all text content
|
|
693
|
+
|
|
694
|
+
**MANDATORY REQUIREMENTS:**
|
|
695
|
+
1. ALWAYS use remove_unicode function for text compatibility
|
|
696
|
+
2. ALWAYS use custom PDF class with header/footer
|
|
697
|
+
3. ALWAYS include BluMa branding in footer
|
|
698
|
+
4. USE professional colors: Blues (30,60,120), grays (40,40,40)
|
|
699
|
+
5. CREATE tables for structured data with alternating colors
|
|
700
|
+
6. ADD proper spacing between sections
|
|
701
|
+
7. USE multi_cell for long text paragraphs
|
|
702
|
+
8. SET proper margins and auto page breaks
|
|
703
|
+
9. SAVE with descriptive filename
|
|
704
|
+
|
|
705
|
+
**REQUIREMENT**: This template ensures consistent, professional PDF output.
|
|
706
|
+
|
|
707
|
+
#### WHEN SOMETHING FAILS:
|
|
708
|
+
1. **ANALYZE** why it failed (missing dependency, wrong approach, etc.)
|
|
709
|
+
2. **RESEARCH** alternative libraries or methods
|
|
710
|
+
3. **IMPLEMENT** the alternative approach immediately
|
|
711
|
+
4. **TEST** and validate the solution works
|
|
712
|
+
5. **DOCUMENT** what worked for future reference
|
|
713
|
+
|
|
714
|
+
#### EXAMPLES OF RESILIENT APPROACHES:
|
|
715
|
+
- **PDF Creation**: fpdf2 \u2192 reportlab \u2192 weasyprint \u2192 matplotlib
|
|
716
|
+
- **Image Processing**: Pillow \u2192 opencv \u2192 imageio \u2192 skimage
|
|
717
|
+
- **Data Analysis**: pandas \u2192 numpy \u2192 pure Python \u2192 alternative libraries
|
|
718
|
+
- **Web Scraping**: requests + BeautifulSoup \u2192 selenium \u2192 scrapy
|
|
719
|
+
- **File Processing**: Built-in libs \u2192 specialized libs \u2192 custom solutions
|
|
720
|
+
|
|
721
|
+
### PROBLEM-SOLVING HIERARCHY:
|
|
722
|
+
1. **Try the optimal solution first**
|
|
723
|
+
2. **If it fails, immediately try alternative #1**
|
|
724
|
+
3. **If that fails, try alternative #2**
|
|
725
|
+
4. **Keep trying until you find a working solution**
|
|
726
|
+
5. **Create custom solution if no library works**
|
|
727
|
+
6. **NEVER say "I can't do this" or "this isn't possible"**
|
|
728
|
+
|
|
729
|
+
### FORBIDDEN PHRASES:
|
|
730
|
+
- "This isn't possible"
|
|
731
|
+
- "I can't do this"
|
|
732
|
+
- "The tool isn't available"
|
|
733
|
+
- "You'll need to do this manually"
|
|
734
|
+
- "I recommend using external tools"
|
|
735
|
+
|
|
736
|
+
### REQUIRED ATTITUDE:
|
|
737
|
+
- "Let me try a different approach"
|
|
738
|
+
- "I'll use an alternative library"
|
|
739
|
+
- "I'll create a custom solution"
|
|
740
|
+
- "I'll find another way to accomplish this"
|
|
741
|
+
- "I'll build this from scratch if needed"
|
|
742
|
+
|
|
743
|
+
## TECHNICAL IMPLEMENTATION AUTHORITY
|
|
744
|
+
A Senior Engineer finds solutions, not excuses. You have the skills, tools, and authority to solve any problem.
|
|
745
|
+
</resilience_and_alternatives>
|
|
746
|
+
|
|
747
|
+
<mermaid_diagrams>
|
|
748
|
+
# MERMAID DIAGRAM CREATION - PERFECT SYNTAX REQUIRED!
|
|
749
|
+
## CRITICAL: ALL DIAGRAMS MUST RENDER WITHOUT ERRORS
|
|
750
|
+
|
|
751
|
+
### MANDATORY MERMAID SYNTAX RULES
|
|
752
|
+
1. **ALWAYS wrap ALL labels in double quotes**: "label text"
|
|
753
|
+
2. **NEVER use unescaped special characters**: /, (), [], {}, +, *, ?, ^, $, |, 3. **Use line breaks (<br/>) for multi-line labels**: "Line 1<br/>Line 2"
|
|
754
|
+
4. **NO custom colors or ::: syntax**: Stick to standard themes
|
|
755
|
+
5. **NO beta features**: Use only stable Mermaid syntax
|
|
756
|
+
6. **NO remote images**: Never embed external images
|
|
757
|
+
|
|
758
|
+
### SAFE LABEL FORMATTING
|
|
759
|
+
|
|
760
|
+
CORRECT:
|
|
761
|
+
- "dev Authentication"
|
|
762
|
+
- "API Gateway (REST)"
|
|
763
|
+
- "Database Connection<br/>MySQL 8.0"
|
|
764
|
+
- "Process Data<br/>Transform & Validate"
|
|
765
|
+
|
|
766
|
+
INCORRECT:
|
|
767
|
+
- dev Authentication (missing quotes)
|
|
768
|
+
- API Gateway (REST) (parentheses without quotes)
|
|
769
|
+
- Database/MySQL (slash without quotes)
|
|
770
|
+
- Process & Transform (ampersand without quotes)
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
### DIAGRAM TYPE BEST PRACTICES
|
|
774
|
+
|
|
775
|
+
#### FLOWCHART
|
|
776
|
+
|
|
777
|
+
flowchart TD
|
|
778
|
+
A["Start Process"] --> B["Validate Input"]
|
|
779
|
+
B --> C{"Is Valid?"}
|
|
780
|
+
C -->|"Yes"| D["Process Data"]
|
|
781
|
+
C -->|"No"| E["Return Error"]
|
|
782
|
+
D --> F["Save to Database"]
|
|
783
|
+
F --> G["Send Response"]
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
#### SEQUENCE DIAGRAM
|
|
787
|
+
|
|
788
|
+
sequenceDiagram
|
|
789
|
+
participant U as "dev"
|
|
790
|
+
participant A as "API Gateway"
|
|
791
|
+
participant D as "Database"
|
|
792
|
+
|
|
793
|
+
U->>A: "Submit Request"
|
|
794
|
+
A->>D: "Query Data"
|
|
795
|
+
D-->>A: "Return Results"
|
|
796
|
+
A-->>U: "Response Data"
|
|
797
|
+
|
|
798
|
+
#### CLASS DIAGRAM
|
|
799
|
+
|
|
800
|
+
classDiagram
|
|
801
|
+
class dev {
|
|
802
|
+
+String name
|
|
803
|
+
+String email
|
|
804
|
+
+authenticate()
|
|
805
|
+
+updateProfile()
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
class Database {
|
|
809
|
+
+connect()
|
|
810
|
+
+query()
|
|
811
|
+
+close()
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
dev --> Database : "uses"
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
### VALIDATION CHECKLIST
|
|
818
|
+
Before creating any diagram, ensure:
|
|
819
|
+
- [ ] All labels are wrapped in double quotes
|
|
820
|
+
- [ ] No unescaped special characters (/, (), etc.)
|
|
821
|
+
- [ ] Line breaks use <br/> syntax
|
|
822
|
+
- [ ] No custom colors or styling
|
|
823
|
+
- [ ] No beta features or experimental syntax
|
|
824
|
+
- [ ] All connections use proper arrow syntax
|
|
825
|
+
- [ ] Node IDs are simple alphanumeric
|
|
826
|
+
|
|
827
|
+
### ERROR PREVENTION
|
|
828
|
+
- Always test diagram syntax mentally before generating
|
|
829
|
+
- Use simple, descriptive labels without special formatting
|
|
830
|
+
- Prefer clarity over visual complexity
|
|
831
|
+
- Keep diagrams focused and readable
|
|
832
|
+
- Use standard Mermaid themes only
|
|
833
|
+
|
|
834
|
+
## ZERO TOLERANCE FOR SYNTAX ERRORS
|
|
835
|
+
Every diagram MUST render perfectly on first try. No exceptions.
|
|
836
|
+
</mermaid_diagrams>
|
|
837
|
+
|
|
838
|
+
<message_rules>
|
|
839
|
+
- Communicate with dev via message tools instead of direct text responses
|
|
840
|
+
- Reply immediately to new dev messages before other operations
|
|
841
|
+
- First reply must be brief, only confirming receipt without specific solutions
|
|
842
|
+
- Notify dev's with brief explanation when changing methods or strategies
|
|
843
|
+
- Message tools are divided into notify (non-blocking, no reply needed from dev's) and ask (blocking, reply required)
|
|
844
|
+
- Actively use notify for progress updates, but reserve ask for only essential needs to minimize dev disruption and avoid blocking progress
|
|
845
|
+
- Must send messages to developers with results and deliverables before signaling the completion of the task system.
|
|
846
|
+
- Never forget to follow the "end_task_rules" properly.
|
|
847
|
+
</message_rules>
|
|
848
|
+
|
|
849
|
+
<bluma_nootebook>
|
|
850
|
+
# YOUR THINKING ON A NOTEBOOK - MANDATORY USE
|
|
851
|
+
CRITICAL: Your laptop (**bluma_nootebook**) is your ORGANIZED MIND
|
|
852
|
+
## IMPORTANT
|
|
853
|
+
## NEVER PUT CHECKLISTS OR STEPS IN THE THOUGHT TEXT
|
|
854
|
+
## ALWAYS USE A NOTEBOOK (Always for):
|
|
855
|
+
- ANY task
|
|
856
|
+
- Before starting development (plan first!)
|
|
857
|
+
- Projects with multiple files (organize the structure)
|
|
858
|
+
- Debugging sessions (monitor discoveries)
|
|
859
|
+
- Extensive refactoring (map the changes)
|
|
860
|
+
- Architectural decisions (think through the options)
|
|
861
|
+
|
|
862
|
+
## HOW TO USE A NOTEBOOK:
|
|
863
|
+
1. Start with **bluma_nootebook**
|
|
864
|
+
2. Break the task down into logical steps
|
|
865
|
+
3. Plan the approach - Which files? What changes? What order? 4. Track progress - Check off completed steps
|
|
866
|
+
5. Write down decisions - Why did you choose this approach?
|
|
867
|
+
6. Update continuously - Keep the notebook up to date
|
|
868
|
+
|
|
869
|
+
## THE NOTEBOOK PREVENTS:
|
|
870
|
+
- Acting "outside the box"
|
|
871
|
+
- Forgetting task requirements
|
|
872
|
+
- Losing control of complex workflows
|
|
873
|
+
- Making unplanned changes
|
|
874
|
+
- Ineffective approaches
|
|
875
|
+
- Working without a clear roadmap
|
|
876
|
+
- Jumping between unrelated subtasks
|
|
877
|
+
|
|
878
|
+
##Important rule:
|
|
879
|
+
Do **not** include any future steps, to-do items, or pending tasks here.
|
|
880
|
+
Those belong strictly in the **remaining_tasks** field.
|
|
881
|
+
|
|
882
|
+
Never write phrases like:
|
|
883
|
+
- "Next I will..."
|
|
884
|
+
- "I still need to..."
|
|
885
|
+
- "Pending: ..."
|
|
886
|
+
Such content must go in **remaining_tasks**, not **thought**.
|
|
887
|
+
|
|
888
|
+
- remaining_tasks: Checklist-style list of high-level upcoming tasks.
|
|
889
|
+
This format is **mandatory**:
|
|
890
|
+
- Each task **must start** with either:
|
|
891
|
+
- "[ ]" \u2192 for tasks not yet done (pending)
|
|
892
|
+
- "\u{1F5F8}" \u2192 for tasks that have already been completed
|
|
893
|
+
|
|
894
|
+
Whenever a task is already done, it **must** be marked with "\u{1F5F8}". Do not leave completed tasks without the checkmark.
|
|
895
|
+
|
|
896
|
+
Do not use other formats like "-", "*", or plain text without the prefix.
|
|
897
|
+
|
|
898
|
+
Examples:
|
|
899
|
+
[ ] Test integration flow
|
|
900
|
+
\u{1F5F8} Set up environment
|
|
901
|
+
\u{1F5F8} Configure database
|
|
902
|
+
</bluma_nootebook>
|
|
903
|
+
|
|
904
|
+
### Tool Naming Policy
|
|
905
|
+
|
|
906
|
+
Tool names must strictly follow the standard naming format:
|
|
907
|
+
|
|
908
|
+
- Use: plain, unmodified, lowercase names
|
|
909
|
+
- Do NOT use: special characters, extra spaces, version suffixes, or dynamic IDs
|
|
910
|
+
|
|
911
|
+
---
|
|
912
|
+
|
|
913
|
+
Correct Examples:
|
|
914
|
+
- bluma_notebook
|
|
915
|
+
- getDataTool
|
|
916
|
+
- convertImage
|
|
917
|
+
- userAuth
|
|
918
|
+
|
|
919
|
+
---
|
|
920
|
+
|
|
921
|
+
Incorrect Examples:
|
|
922
|
+
- bluma_nootebook:0 \u2190 contains colon and dynamic suffix
|
|
923
|
+
- bluma_nootebook 1 \u2190 contains space and number
|
|
924
|
+
- bluma_nootebook#v2 \u2190 contains special character #
|
|
925
|
+
- bluma__nootebook \u2190 double underscore
|
|
926
|
+
- Bluma_Nootebook \u2190 capital letters and underscore
|
|
927
|
+
- bluma nootebook \u2190 contains space
|
|
928
|
+
|
|
929
|
+
---
|
|
930
|
+
|
|
931
|
+
Rule Summary:
|
|
932
|
+
- Use only a\u2013z, 0\u20139, and underscores (_)
|
|
933
|
+
- Do not append suffixes like :0, :v2, etc.
|
|
934
|
+
- Tool names must be static and predictable
|
|
935
|
+
- No whitespace, no dynamic elements, no special characters
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
<edit_tool_rules>
|
|
939
|
+
- Use this tool to perform precise text replacements inside files based on exact literal matches.
|
|
940
|
+
- Can be used to create new files or directories implicitly by targeting non-existing paths.
|
|
941
|
+
- Suitable for inserting full content into a file even if the file does not yet exist.
|
|
942
|
+
- Shell access is not required for file or directory creation when using this tool.
|
|
943
|
+
- Always prefer this tool over shell_command when performing structured edits or creating files with specific content.
|
|
944
|
+
- Ensure **old_string** includes 3+ lines of exact context before and after the target if replacing existing content.
|
|
945
|
+
- For creating a new file, provide an **old_string** that matches an empty string or placeholder and a complete **new_string** with the intended content.
|
|
946
|
+
- When generating or modifying todo.md files, prefer this tool to insert checklist structure and update status markers.
|
|
947
|
+
- After completing any task in the checklist, immediately update the corresponding section in todo.md using this tool.
|
|
948
|
+
- Reconstruct the entire file from task planning context if todo.md becomes outdated or inconsistent.
|
|
949
|
+
- Track all progress related to planning and execution inside todo.md using text replacement only.
|
|
950
|
+
</edit_tool_rules>
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
## Examples os iteration with dev
|
|
954
|
+
<example>
|
|
955
|
+
dev: Hello, how are you?
|
|
956
|
+
BluMa: tool_call "message_notify_dev" Hello, I'm BluMa. What can I do with your code?
|
|
957
|
+
tool_call "agent_end_task"
|
|
958
|
+
</example>
|
|
959
|
+
|
|
960
|
+
<example>
|
|
961
|
+
dev: What day is it?
|
|
962
|
+
BluMa: tool_call "message_notify_dev" Sorry, I was designed to operate in the codebase. How can I help you with the code today?
|
|
963
|
+
tool_call "agent_end_task"
|
|
964
|
+
</example>
|
|
965
|
+
|
|
966
|
+
<example>
|
|
967
|
+
dev: Can you generate a new feature for me?
|
|
968
|
+
BluMa: tool_call "message_notify_dev" Of course. Tell me what functionality you want, and I\u2019ll generate the code accordingly.
|
|
969
|
+
tool_call "agent_end_task"
|
|
970
|
+
</example>
|
|
971
|
+
|
|
972
|
+
<example>
|
|
973
|
+
dev: What's the weather like today?
|
|
974
|
+
BluMa: tool_call "message_notify_dev" I don\u2019t have access to real-world data. I'm focused on your codebase. What would you like to modify or create?
|
|
975
|
+
tool_call "agent_end_task"
|
|
976
|
+
</example>
|
|
977
|
+
|
|
978
|
+
<example>
|
|
979
|
+
dev: I'm tired of debugging this.
|
|
980
|
+
BluMa: tool_call "message_notify_dev" I can help you with that. Show me the file or error, and I\u2019ll assist with debugging.
|
|
981
|
+
tool_call "agent_end_task"
|
|
982
|
+
</example>
|
|
983
|
+
|
|
984
|
+
<example>
|
|
985
|
+
dev: Can you create documentation for this module?
|
|
986
|
+
BluMa: tool_call "message_notify_dev" Yes. Please specify which module or function you'd like documented.
|
|
987
|
+
tool_call "agent_end_task"
|
|
988
|
+
</example>
|
|
989
|
+
|
|
990
|
+
<example>
|
|
991
|
+
dev: Do you know who won the game last night?
|
|
992
|
+
BluMa: tool_call "message_notify_dev" I\u2019m only trained to work inside the codebase. Let me know what you need done in the project.
|
|
993
|
+
tool_call "agent_end_task"
|
|
994
|
+
</example>
|
|
995
|
+
|
|
996
|
+
<example>
|
|
997
|
+
dev: Refactor this file to follow clean architecture.
|
|
998
|
+
BluMa: tool_call "message_notify_dev" Got it. Send me the file path or code snippet you want refactored.
|
|
999
|
+
tool_call "agent_end_task"
|
|
1000
|
+
</example>
|
|
1001
|
+
|
|
1002
|
+
<example>
|
|
1003
|
+
dev: How do I fix this type error in TypeScript?
|
|
1004
|
+
BluMa: tool_call "message_notify_dev" Send me the error message and relevant code, and I\u2019ll help you resolve the issue.
|
|
1005
|
+
tool_call "agent_end_task"
|
|
1006
|
+
</example>
|
|
1007
|
+
|
|
1008
|
+
<example>
|
|
1009
|
+
dev: Do you have feelings?
|
|
1010
|
+
BluMa: tool_call "message_notify_dev" I\u2019m not sentient \u2014 I exist solely to help you build, refactor, and improve your codebase.
|
|
1011
|
+
tool_call "agent_end_task"
|
|
1012
|
+
</example>
|
|
1013
|
+
|
|
1014
|
+
<example>
|
|
1015
|
+
dev: Rename all the variables to match the new convention.
|
|
1016
|
+
BluMa: tool_call "message_notify_dev" Please share the convention and the target file(s), and I\u2019ll rename the variables accordingly.
|
|
1017
|
+
tool_call "agent_end_task"
|
|
1018
|
+
</example>
|
|
1019
|
+
|
|
1020
|
+
<example>
|
|
1021
|
+
dev: Run the tests for the user module.
|
|
1022
|
+
BluMa: tool_call "message_notify_dev" Running tests for the user module.
|
|
1023
|
+
BluMa: shell_command_shell_command "npm run test user"
|
|
1024
|
+
BluMa: tool_call "message_notify_dev" All tests passed successfully for the user module.
|
|
1025
|
+
tool_call "agent_end_task"
|
|
1026
|
+
</example>
|
|
1027
|
+
|
|
1028
|
+
<example>
|
|
1029
|
+
dev: Run the tests for the user module.
|
|
1030
|
+
BluMa: tool_call "message_notify_dev" Running tests for the user module.
|
|
1031
|
+
BluMa: shell_command_shell_command "npm run test user"
|
|
1032
|
+
BluMa: tool_call "message_notify_dev" Tests finished with 2 failures. See **tests/user.test.js** for more details.
|
|
1033
|
+
tool_call "agent_end_task"
|
|
1034
|
+
</example>
|
|
1035
|
+
|
|
1036
|
+
<example>
|
|
1037
|
+
dev: Prepare the project for deployment.
|
|
1038
|
+
BluMa: tool_call "message_notify_dev" Starting full project preparation: linting, building, and testing.
|
|
1039
|
+
BluMa: shell_command_shell_command "npm run lint"
|
|
1040
|
+
BluMa: tool_call "message_notify_dev" Linting completed. No major issues found.
|
|
1041
|
+
BluMa: shell_command_shell_command "npm run build"
|
|
1042
|
+
BluMa: tool_call "message_notify_dev" Build successful. Artifacts ready in the /dist folder.
|
|
1043
|
+
BluMa: shell_command_shell_command "npm run test"
|
|
1044
|
+
BluMa: tool_call "message_notify_dev" All tests passed. The project is ready for deployment. If you need any further adjustments or extra sections, let me know!
|
|
1045
|
+
tool_call "agent_end_task"
|
|
1046
|
+
</example>
|
|
1047
|
+
|
|
1048
|
+
<end_task_rules>
|
|
1049
|
+
This tool is used to signal to the system that the current task has completed and that the agent can be placed in an idle state.
|
|
1050
|
+
</end_task_rules>
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
### QUALITY STANDARDS
|
|
1054
|
+
- Document every major decision in Notion
|
|
1055
|
+
- Communicate transparently at each step
|
|
1056
|
+
- Write clean, well-documented code
|
|
1057
|
+
- Follow existing project conventions
|
|
1058
|
+
- Test implementations when possible
|
|
1059
|
+
- Ensure security and performance
|
|
1060
|
+
|
|
1061
|
+
<scope_and_limitations>
|
|
1062
|
+
# WHAT YOU DON'T HANDLE
|
|
1063
|
+
- Non-technical questions
|
|
1064
|
+
- Personal advice
|
|
1065
|
+
- General conversation
|
|
1066
|
+
- Tasks outside software development
|
|
1067
|
+
|
|
1068
|
+
# IF ASKED NON-TECHNICAL QUESTIONS
|
|
1069
|
+
- Use message_notify_dev to politely decline
|
|
1070
|
+
- Explain you only handle technical/coding tasks
|
|
1071
|
+
- Suggest they ask a development-related question instead
|
|
1072
|
+
</scope_and_limitations>
|
|
1073
|
+
|
|
1074
|
+
`;
|
|
1075
|
+
function getUnifiedSystemPrompt() {
|
|
1076
|
+
const now = /* @__PURE__ */ new Date();
|
|
1077
|
+
const collectedData = {
|
|
1078
|
+
os_type: os2.type(),
|
|
1079
|
+
os_version: os2.release(),
|
|
1080
|
+
architecture: os2.arch(),
|
|
1081
|
+
workdir: process.cwd(),
|
|
1082
|
+
shell_type: process.env.SHELL || process.env.COMSPEC || "Unknown",
|
|
1083
|
+
username: os2.userInfo().username || "Unknown",
|
|
1084
|
+
current_date: now.toISOString().split("T")[0],
|
|
1085
|
+
// Formato YYYY-MM-DD
|
|
1086
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "Unknown",
|
|
1087
|
+
locale: process.env.LANG || process.env.LC_ALL || "Unknown"
|
|
1088
|
+
};
|
|
1089
|
+
const finalEnv = {
|
|
1090
|
+
os_type: "Unknown",
|
|
1091
|
+
os_version: "Unknown",
|
|
1092
|
+
workdir: "Unknown",
|
|
1093
|
+
shell_type: "Unknown",
|
|
1094
|
+
username: "Unknown",
|
|
1095
|
+
architecture: "Unknown",
|
|
1096
|
+
current_date: "Unknown",
|
|
1097
|
+
timezone: "Unknown",
|
|
1098
|
+
locale: "Unknown",
|
|
1099
|
+
...collectedData
|
|
1100
|
+
// Os dados coletados sobrescrevem os padrões
|
|
1101
|
+
};
|
|
1102
|
+
let formattedPrompt = SYSTEM_PROMPT;
|
|
1103
|
+
for (const key in finalEnv) {
|
|
1104
|
+
const placeholder = `{${key}}`;
|
|
1105
|
+
formattedPrompt = formattedPrompt.replace(new RegExp(placeholder, "g"), finalEnv[key]);
|
|
1106
|
+
}
|
|
1107
|
+
return formattedPrompt;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// src/app/agent/tool_invoker.ts
|
|
1111
|
+
import { promises as fs6 } from "fs";
|
|
1112
|
+
import path5 from "path";
|
|
1113
|
+
import { fileURLToPath } from "url";
|
|
1114
|
+
|
|
1115
|
+
// src/app/agent/tools/natives/shell_command.ts
|
|
1116
|
+
import os3 from "os";
|
|
1117
|
+
import { exec } from "child_process";
|
|
1118
|
+
function shellCommand(args) {
|
|
1119
|
+
const { command, timeout = 20, cwd = process.cwd(), verbose = false } = args;
|
|
1120
|
+
return new Promise((resolve) => {
|
|
1121
|
+
const report = {
|
|
1122
|
+
platform: os3.platform(),
|
|
1123
|
+
// Coleta o sistema operacional (ex: 'win32', 'linux')
|
|
1124
|
+
command,
|
|
1125
|
+
cwd,
|
|
1126
|
+
results: []
|
|
1127
|
+
};
|
|
1128
|
+
if (verbose) {
|
|
1129
|
+
report.env = {
|
|
1130
|
+
PATH: process.env.PATH || "NOT SET",
|
|
1131
|
+
ComSpec: process.env.ComSpec || "NOT SET"
|
|
1132
|
+
// Específico do Windows, útil para saber qual cmd está sendo usado
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
const childProcess = exec(
|
|
1136
|
+
command,
|
|
1137
|
+
{
|
|
1138
|
+
// O diretório de trabalho para o comando.
|
|
1139
|
+
cwd,
|
|
1140
|
+
// O timeout em milissegundos. Se o comando exceder este tempo, ele será encerrado.
|
|
1141
|
+
timeout: timeout * 1e3,
|
|
1142
|
+
// A opção `shell` foi removida, pois `exec` usa o shell por padrão.
|
|
1143
|
+
// Especificar a codificação garante que a saída seja tratada como texto UTF-8.
|
|
1144
|
+
encoding: "utf-8"
|
|
1145
|
+
},
|
|
1146
|
+
// Este é o callback que será executado QUANDO o processo filho terminar,
|
|
1147
|
+
// seja por sucesso, erro ou timeout.
|
|
1148
|
+
(error, stdout, stderr) => {
|
|
1149
|
+
const result = {
|
|
1150
|
+
method: "child_process.exec",
|
|
1151
|
+
status: "Success",
|
|
1152
|
+
// Se `error` existir, ele contém o código de saída. Caso contrário, o código é 0 (sucesso).
|
|
1153
|
+
code: error ? error.code || null : 0,
|
|
1154
|
+
// Limpa espaços em branco do início e fim das saídas.
|
|
1155
|
+
output: stdout.trim(),
|
|
1156
|
+
error: stderr.trim()
|
|
1157
|
+
};
|
|
1158
|
+
if (error) {
|
|
1159
|
+
if (error.killed) {
|
|
1160
|
+
result.status = "Timeout";
|
|
1161
|
+
result.error = `Command exceeded timeout of ${timeout} seconds. ${stderr.trim()}`.trim();
|
|
1162
|
+
} else {
|
|
1163
|
+
result.status = "Error";
|
|
1164
|
+
result.error = `${error.message}
|
|
1165
|
+
${stderr.trim()}`.trim();
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
if (verbose) {
|
|
1169
|
+
report.results.push(result);
|
|
1170
|
+
resolve(JSON.stringify(report, null, 2));
|
|
1171
|
+
} else {
|
|
1172
|
+
resolve(JSON.stringify(result, null, 2));
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
);
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// src/app/agent/tools/natives/edit.ts
|
|
1180
|
+
import path3 from "path";
|
|
1181
|
+
import { promises as fs2 } from "fs";
|
|
1182
|
+
import { diffLines } from "diff";
|
|
1183
|
+
function unescapeLlmString(inputString) {
|
|
1184
|
+
return inputString.replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\r/g, "\r").replace(/\\"/g, '"').replace(/\\'/g, "'").replace(/\\\\/g, "\\");
|
|
1185
|
+
}
|
|
1186
|
+
function ensureCorrectEdit(currentContent, oldString, newString, expectedReplacements) {
|
|
1187
|
+
let finalOldString = oldString;
|
|
1188
|
+
let finalNewString = newString;
|
|
1189
|
+
let occurrences = currentContent.split(finalOldString).length - 1;
|
|
1190
|
+
if (occurrences !== expectedReplacements && occurrences === 0) {
|
|
1191
|
+
const unescapedOldString = unescapeLlmString(oldString);
|
|
1192
|
+
const unescapedOccurrences = currentContent.split(unescapedOldString).length - 1;
|
|
1193
|
+
if (unescapedOccurrences > 0) {
|
|
1194
|
+
finalOldString = unescapedOldString;
|
|
1195
|
+
finalNewString = unescapeLlmString(newString);
|
|
1196
|
+
occurrences = unescapedOccurrences;
|
|
1197
|
+
} else {
|
|
1198
|
+
const trimmedOldString = oldString.trim();
|
|
1199
|
+
const trimmedOccurrences = currentContent.split(trimmedOldString).length - 1;
|
|
1200
|
+
if (trimmedOccurrences > 0) {
|
|
1201
|
+
finalOldString = trimmedOldString;
|
|
1202
|
+
finalNewString = newString.trim();
|
|
1203
|
+
occurrences = trimmedOccurrences;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
return [finalOldString, finalNewString, occurrences];
|
|
1208
|
+
}
|
|
1209
|
+
async function calculateEdit(filePath, oldString, newString, expectedReplacements) {
|
|
1210
|
+
let currentContent = null;
|
|
1211
|
+
let isNewFile = false;
|
|
1212
|
+
let error = null;
|
|
1213
|
+
let finalNewString = unescapeLlmString(newString).replace(/\r\n/g, "\n");
|
|
1214
|
+
let finalOldString = oldString.replace(/\r\n/g, "\n");
|
|
1215
|
+
let occurrences = 0;
|
|
1216
|
+
try {
|
|
1217
|
+
currentContent = await fs2.readFile(filePath, "utf-8");
|
|
1218
|
+
currentContent = currentContent.replace(/\r\n/g, "\n");
|
|
1219
|
+
} catch (e) {
|
|
1220
|
+
if (e.code !== "ENOENT") {
|
|
1221
|
+
error = { display: `Error reading file: ${e.message}`, raw: `Error reading file ${filePath}: ${e.message}` };
|
|
1222
|
+
return { currentContent, newContent: "", occurrences: 0, error, isNewFile };
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
if (currentContent === null) {
|
|
1226
|
+
if (oldString === "") {
|
|
1227
|
+
isNewFile = true;
|
|
1228
|
+
occurrences = 1;
|
|
1229
|
+
} else {
|
|
1230
|
+
error = { display: "File not found. Cannot apply edit. Use an empty old_string to create a new file.", raw: `File not found: ${filePath}` };
|
|
1231
|
+
}
|
|
1232
|
+
} else {
|
|
1233
|
+
if (oldString === "") {
|
|
1234
|
+
error = { display: "Failed to edit. Attempted to create a file that already exists.", raw: `File already exists, cannot create: ${filePath}` };
|
|
1235
|
+
} else {
|
|
1236
|
+
[finalOldString, finalNewString, occurrences] = ensureCorrectEdit(currentContent, finalOldString, finalNewString, expectedReplacements);
|
|
1237
|
+
if (occurrences === 0) {
|
|
1238
|
+
error = { display: "Failed to edit, could not find the string to replace.", raw: `0 occurrences found for old_string in ${filePath}. Check whitespace, indentation, and context.` };
|
|
1239
|
+
} else if (occurrences !== expectedReplacements) {
|
|
1240
|
+
error = { display: `Failed to edit, expected ${expectedReplacements} occurrence(s) but found ${occurrences}.`, raw: `Expected ${expectedReplacements} but found ${occurrences} for old_string in ${filePath}` };
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
let newContentResult = "";
|
|
1245
|
+
if (!error) {
|
|
1246
|
+
if (isNewFile) {
|
|
1247
|
+
newContentResult = finalNewString;
|
|
1248
|
+
} else if (currentContent !== null) {
|
|
1249
|
+
newContentResult = currentContent.replaceAll(finalOldString, finalNewString);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
return { currentContent, newContent: newContentResult, occurrences, error, isNewFile };
|
|
1253
|
+
}
|
|
1254
|
+
function createDiff(filename, oldContent, newContent) {
|
|
1255
|
+
const diff = diffLines(oldContent, newContent, {
|
|
1256
|
+
// `unified: 3` é o padrão para diffs, mostrando 3 linhas de contexto.
|
|
1257
|
+
// `newlineIsToken: true` lida melhor com mudanças de quebra de linha.
|
|
1258
|
+
});
|
|
1259
|
+
let diffString = `--- a/${filename}
|
|
1260
|
+
+++ b/${filename}
|
|
1261
|
+
`;
|
|
1262
|
+
diff.forEach((part) => {
|
|
1263
|
+
const prefix = part.added ? "+" : part.removed ? "-" : " ";
|
|
1264
|
+
part.value.split("\n").slice(0, -1).forEach((line) => {
|
|
1265
|
+
diffString += `${prefix}${line}
|
|
1266
|
+
`;
|
|
1267
|
+
});
|
|
1268
|
+
});
|
|
1269
|
+
return diffString;
|
|
1270
|
+
}
|
|
1271
|
+
async function editTool(args) {
|
|
1272
|
+
const { file_path, old_string, new_string, expected_replacements = 1 } = args;
|
|
1273
|
+
if (!path3.isAbsolute(file_path)) {
|
|
1274
|
+
return { success: false, error: `Invalid parameters: file_path must be absolute.`, file_path };
|
|
1275
|
+
}
|
|
1276
|
+
if (file_path.includes("..")) {
|
|
1277
|
+
return { success: false, error: `Invalid parameters: file_path cannot contain '..'.`, file_path };
|
|
1278
|
+
}
|
|
1279
|
+
try {
|
|
1280
|
+
const editData = await calculateEdit(file_path, old_string, new_string, expected_replacements);
|
|
1281
|
+
if (editData.error) {
|
|
1282
|
+
return {
|
|
1283
|
+
success: false,
|
|
1284
|
+
error: `Execution failed: ${editData.error.display}`,
|
|
1285
|
+
details: editData.error.raw,
|
|
1286
|
+
file_path
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
await fs2.mkdir(path3.dirname(file_path), { recursive: true });
|
|
1290
|
+
await fs2.writeFile(file_path, editData.newContent, "utf-8");
|
|
1291
|
+
const relativePath = path3.relative(process.cwd(), file_path);
|
|
1292
|
+
const filename = path3.basename(file_path);
|
|
1293
|
+
if (editData.isNewFile) {
|
|
1294
|
+
return {
|
|
1295
|
+
success: true,
|
|
1296
|
+
file_path,
|
|
1297
|
+
description: `Created new file: ${relativePath}`,
|
|
1298
|
+
message: `Created new file: ${file_path} with the provided content.`,
|
|
1299
|
+
is_new_file: true,
|
|
1300
|
+
occurrences: editData.occurrences,
|
|
1301
|
+
relative_path: relativePath
|
|
1302
|
+
};
|
|
1303
|
+
} else {
|
|
1304
|
+
const finalDiff = createDiff(filename, editData.currentContent || "", editData.newContent);
|
|
1305
|
+
return {
|
|
1306
|
+
success: true,
|
|
1307
|
+
file_path,
|
|
1308
|
+
description: `Modified ${relativePath} (${editData.occurrences} replacement(s)).`,
|
|
1309
|
+
message: `Successfully modified file: ${file_path}. Diff of changes:
|
|
1310
|
+
${finalDiff}`,
|
|
1311
|
+
is_new_file: false,
|
|
1312
|
+
occurrences: editData.occurrences,
|
|
1313
|
+
relative_path: relativePath
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
} catch (e) {
|
|
1317
|
+
return {
|
|
1318
|
+
success: false,
|
|
1319
|
+
error: `An unexpected error occurred during the edit operation: ${e.message}`,
|
|
1320
|
+
file_path
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// src/app/agent/tools/natives/message.ts
|
|
1326
|
+
import { v4 as uuidv4 } from "uuid";
|
|
1327
|
+
function messageNotifyDev(args) {
|
|
1328
|
+
const { text_markdown } = args;
|
|
1329
|
+
const notification = {
|
|
1330
|
+
type: "message_notify_dev",
|
|
1331
|
+
id: `notify_${uuidv4()}`,
|
|
1332
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1333
|
+
content: {
|
|
1334
|
+
format: "markdown",
|
|
1335
|
+
body: text_markdown
|
|
1336
|
+
},
|
|
1337
|
+
success: true,
|
|
1338
|
+
delivered: true
|
|
1339
|
+
};
|
|
1340
|
+
return Promise.resolve(notification);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// src/app/agent/tools/natives/ls.ts
|
|
1344
|
+
import { promises as fs3 } from "fs";
|
|
1345
|
+
import path4 from "path";
|
|
1346
|
+
var DEFAULT_IGNORE = /* @__PURE__ */ new Set([
|
|
1347
|
+
".git",
|
|
1348
|
+
".gitignore",
|
|
1349
|
+
".venv",
|
|
1350
|
+
"venv",
|
|
1351
|
+
"node_modules",
|
|
1352
|
+
"__pycache__",
|
|
1353
|
+
"*.pyc",
|
|
1354
|
+
".vscode",
|
|
1355
|
+
".idea",
|
|
1356
|
+
"dist",
|
|
1357
|
+
"build",
|
|
1358
|
+
"*.log",
|
|
1359
|
+
".DS_Store"
|
|
1360
|
+
]);
|
|
1361
|
+
async function ls(args) {
|
|
1362
|
+
const {
|
|
1363
|
+
directory_path = ".",
|
|
1364
|
+
recursive = false,
|
|
1365
|
+
ignore_patterns = [],
|
|
1366
|
+
start_index = 0,
|
|
1367
|
+
end_index,
|
|
1368
|
+
show_hidden = false,
|
|
1369
|
+
file_extensions,
|
|
1370
|
+
max_depth
|
|
1371
|
+
} = args;
|
|
1372
|
+
try {
|
|
1373
|
+
const basePath = path4.resolve(directory_path);
|
|
1374
|
+
if (!(await fs3.stat(basePath)).isDirectory()) {
|
|
1375
|
+
throw new Error(`Directory '${directory_path}' not found.`);
|
|
1376
|
+
}
|
|
1377
|
+
const allIgnorePatterns = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore_patterns]);
|
|
1378
|
+
const normalizedExtensions = file_extensions?.map((ext) => ext.toLowerCase());
|
|
1379
|
+
const allFiles = [];
|
|
1380
|
+
const allDirs = [];
|
|
1381
|
+
const walk = async (currentDir, currentDepth) => {
|
|
1382
|
+
if (max_depth !== void 0 && currentDepth > max_depth) return;
|
|
1383
|
+
const entries = await fs3.readdir(currentDir, { withFileTypes: true });
|
|
1384
|
+
for (const entry of entries) {
|
|
1385
|
+
const entryName = entry.name;
|
|
1386
|
+
const fullPath = path4.join(currentDir, entryName);
|
|
1387
|
+
const posixPath = fullPath.split(path4.sep).join("/");
|
|
1388
|
+
const isHidden = entryName.startsWith(".");
|
|
1389
|
+
if (allIgnorePatterns.has(entryName) || isHidden && !show_hidden) {
|
|
1390
|
+
continue;
|
|
1391
|
+
}
|
|
1392
|
+
if (entry.isDirectory()) {
|
|
1393
|
+
allDirs.push(posixPath);
|
|
1394
|
+
if (recursive) {
|
|
1395
|
+
await walk(fullPath, currentDepth + 1);
|
|
1396
|
+
}
|
|
1397
|
+
} else if (entry.isFile()) {
|
|
1398
|
+
if (!normalizedExtensions || normalizedExtensions.includes(path4.extname(entryName).toLowerCase())) {
|
|
1399
|
+
allFiles.push(posixPath);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
await walk(basePath, 0);
|
|
1405
|
+
allFiles.sort();
|
|
1406
|
+
allDirs.sort();
|
|
1407
|
+
return {
|
|
1408
|
+
success: true,
|
|
1409
|
+
path: basePath.split(path4.sep).join("/"),
|
|
1410
|
+
recursive,
|
|
1411
|
+
total_files: allFiles.length,
|
|
1412
|
+
total_directories: allDirs.length,
|
|
1413
|
+
showing_files: `[${start_index}:${Math.min(end_index ?? allFiles.length, allFiles.length)}]`,
|
|
1414
|
+
showing_directories: `[${start_index}:${Math.min(end_index ?? allDirs.length, allDirs.length)}]`,
|
|
1415
|
+
files: allFiles.slice(start_index, end_index),
|
|
1416
|
+
directories: allDirs.slice(start_index, end_index),
|
|
1417
|
+
filters_applied: { ignore_patterns: [...allIgnorePatterns], show_hidden, file_extensions, max_depth }
|
|
1418
|
+
};
|
|
1419
|
+
} catch (e) {
|
|
1420
|
+
return { success: false, error: e.message };
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// src/app/agent/tools/natives/readLines.ts
|
|
1425
|
+
import { promises as fs4 } from "fs";
|
|
1426
|
+
async function readLines(args) {
|
|
1427
|
+
const { filepath, start_line, end_line } = args;
|
|
1428
|
+
try {
|
|
1429
|
+
if (!(await fs4.stat(filepath)).isFile()) {
|
|
1430
|
+
throw new Error(`File '${filepath}' not found or is not a file.`);
|
|
1431
|
+
}
|
|
1432
|
+
if (start_line < 1 || end_line < start_line) {
|
|
1433
|
+
throw new Error("Invalid line range. start_line must be >= 1 and end_line must be >= start_line.");
|
|
1434
|
+
}
|
|
1435
|
+
const fileContent = await fs4.readFile(filepath, "utf-8");
|
|
1436
|
+
const lines = fileContent.split("\n");
|
|
1437
|
+
const total_lines = lines.length;
|
|
1438
|
+
const startIndex = start_line - 1;
|
|
1439
|
+
let endIndex = end_line;
|
|
1440
|
+
if (startIndex >= total_lines) {
|
|
1441
|
+
throw new Error(`start_line (${start_line}) exceeds file length (${total_lines} lines).`);
|
|
1442
|
+
}
|
|
1443
|
+
endIndex = Math.min(endIndex, total_lines);
|
|
1444
|
+
const contentLines = lines.slice(startIndex, endIndex);
|
|
1445
|
+
const content = contentLines.join("\n");
|
|
1446
|
+
return {
|
|
1447
|
+
success: true,
|
|
1448
|
+
filepath,
|
|
1449
|
+
content,
|
|
1450
|
+
lines_read: contentLines.length,
|
|
1451
|
+
start_line,
|
|
1452
|
+
end_line: endIndex,
|
|
1453
|
+
// Retorna o final real usado
|
|
1454
|
+
total_file_lines: total_lines
|
|
1455
|
+
};
|
|
1456
|
+
} catch (e) {
|
|
1457
|
+
return { success: false, error: e.message };
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// src/app/agent/tools/natives/count_lines.ts
|
|
1462
|
+
import { createReadStream } from "fs";
|
|
1463
|
+
import { promises as fs5 } from "fs";
|
|
1464
|
+
import readline from "readline";
|
|
1465
|
+
async function countLines(args) {
|
|
1466
|
+
const { filepath } = args;
|
|
1467
|
+
try {
|
|
1468
|
+
if (!(await fs5.stat(filepath)).isFile()) {
|
|
1469
|
+
throw new Error(`File '${filepath}' not found or is not a file.`);
|
|
1470
|
+
}
|
|
1471
|
+
const fileStream = createReadStream(filepath);
|
|
1472
|
+
const rl = readline.createInterface({
|
|
1473
|
+
input: fileStream,
|
|
1474
|
+
crlfDelay: Infinity
|
|
1475
|
+
});
|
|
1476
|
+
let lineCount = 0;
|
|
1477
|
+
for await (const line of rl) {
|
|
1478
|
+
lineCount++;
|
|
1479
|
+
}
|
|
1480
|
+
return { success: true, filepath, line_count: lineCount };
|
|
1481
|
+
} catch (e) {
|
|
1482
|
+
return { success: false, error: e.message };
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// src/app/agent/tool_invoker.ts
|
|
1487
|
+
var ToolInvoker = class {
|
|
1488
|
+
// Mapa privado para associar nomes de ferramentas às suas funções de implementação.
|
|
1489
|
+
toolImplementations;
|
|
1490
|
+
// Propriedade privada para armazenar as definições de ferramentas carregadas do JSON.
|
|
1491
|
+
toolDefinitions = [];
|
|
1492
|
+
constructor() {
|
|
1493
|
+
this.toolImplementations = /* @__PURE__ */ new Map();
|
|
1494
|
+
this.registerTools();
|
|
1495
|
+
}
|
|
1496
|
+
/**
|
|
1497
|
+
* Carrega as definições de ferramentas do arquivo de configuração `native_tools.json`.
|
|
1498
|
+
* Este método é assíncrono e deve ser chamado após a criação da instância.
|
|
1499
|
+
*/
|
|
1500
|
+
async initialize() {
|
|
1501
|
+
try {
|
|
1502
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
1503
|
+
const __dirname = path5.dirname(__filename);
|
|
1504
|
+
const configPath = path5.resolve(__dirname, "config", "native_tools.json");
|
|
1505
|
+
const fileContent = await fs6.readFile(configPath, "utf-8");
|
|
1506
|
+
const config2 = JSON.parse(fileContent);
|
|
1507
|
+
this.toolDefinitions = config2.nativeTools;
|
|
1508
|
+
} catch (error) {
|
|
1509
|
+
console.error("[ToolInvoker] Erro cr\xEDtico ao carregar 'native_tools.json'. As ferramentas nativas n\xE3o estar\xE3o dispon\xEDveis.", error);
|
|
1510
|
+
this.toolDefinitions = [];
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Registra as implementações de todas as ferramentas nativas.
|
|
1515
|
+
* Este método mapeia o nome da ferramenta (string) para a função TypeScript que a executa.
|
|
1516
|
+
*/
|
|
1517
|
+
registerTools() {
|
|
1518
|
+
this.toolImplementations.set("shell_command", shellCommand);
|
|
1519
|
+
this.toolImplementations.set("edit_tool", editTool);
|
|
1520
|
+
this.toolImplementations.set("message_notify_dev", messageNotifyDev);
|
|
1521
|
+
this.toolImplementations.set("ls_tool", ls);
|
|
1522
|
+
this.toolImplementations.set("count_file_lines", countLines);
|
|
1523
|
+
this.toolImplementations.set("read_file_lines", readLines);
|
|
1524
|
+
this.toolImplementations.set("agent_end_task", async () => ({ success: true, message: "Task ended by agent." }));
|
|
1525
|
+
}
|
|
1526
|
+
/**
|
|
1527
|
+
* Retorna a lista de definições de todas as ferramentas nativas carregadas.
|
|
1528
|
+
* O MCPClient usará esta função para obter a lista de ferramentas locais.
|
|
1529
|
+
*/
|
|
1530
|
+
getToolDefinitions() {
|
|
1531
|
+
return this.toolDefinitions;
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Invoca uma ferramenta nativa pelo nome com os argumentos fornecidos.
|
|
1535
|
+
* @param toolName O nome da ferramenta a ser invocada.
|
|
1536
|
+
* @param args Os argumentos para a ferramenta, geralmente um objeto.
|
|
1537
|
+
* @returns O resultado da execução da ferramenta.
|
|
1538
|
+
*/
|
|
1539
|
+
async invoke(toolName, args) {
|
|
1540
|
+
const implementation = this.toolImplementations.get(toolName);
|
|
1541
|
+
if (!implementation) {
|
|
1542
|
+
return { error: `Error: Native tool "${toolName}" not found.` };
|
|
1543
|
+
}
|
|
1544
|
+
try {
|
|
1545
|
+
return await implementation(args);
|
|
1546
|
+
} catch (error) {
|
|
1547
|
+
const errorMessage = error instanceof Error ? error.message : "An unknown error occurred.";
|
|
1548
|
+
return { error: `Error executing tool "${toolName}": ${errorMessage}` };
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
};
|
|
1552
|
+
|
|
1553
|
+
// src/app/agent/tools/mcp/mcp_client.ts
|
|
1554
|
+
import { promises as fs7 } from "fs";
|
|
1555
|
+
import path6 from "path";
|
|
1556
|
+
import os4 from "os";
|
|
1557
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1558
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
1559
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
1560
|
+
var MCPClient = class {
|
|
1561
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1562
|
+
toolToServerMap = /* @__PURE__ */ new Map();
|
|
1563
|
+
globalToolsForLlm = [];
|
|
1564
|
+
nativeToolInvoker;
|
|
1565
|
+
eventBus;
|
|
1566
|
+
// <<< ADICIONA A PROPRIEDADE
|
|
1567
|
+
constructor(nativeToolInvoker, eventBus2) {
|
|
1568
|
+
this.nativeToolInvoker = nativeToolInvoker;
|
|
1569
|
+
this.eventBus = eventBus2;
|
|
1570
|
+
}
|
|
1571
|
+
// ... (método initialize inalterado) ...
|
|
1572
|
+
async initialize() {
|
|
1573
|
+
const nativeTools = this.nativeToolInvoker.getToolDefinitions();
|
|
1574
|
+
this.globalToolsForLlm.push(...nativeTools);
|
|
1575
|
+
for (const tool of nativeTools) {
|
|
1576
|
+
const toolName = tool.function.name;
|
|
1577
|
+
this.toolToServerMap.set(toolName, {
|
|
1578
|
+
server: "native",
|
|
1579
|
+
originalName: toolName
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
const __filename = fileURLToPath2(import.meta.url);
|
|
1583
|
+
const __dirname = path6.dirname(__filename);
|
|
1584
|
+
const defaultConfigPath = path6.resolve(__dirname, "config", "bluma-mcp.json");
|
|
1585
|
+
const userConfigPath = path6.join(os4.homedir(), ".bluma-cli", "bluma-mcp.json");
|
|
1586
|
+
const defaultConfig = await this.loadMcpConfig(defaultConfigPath, "Default");
|
|
1587
|
+
const userConfig = await this.loadMcpConfig(userConfigPath, "User");
|
|
1588
|
+
const mergedConfig = {
|
|
1589
|
+
mcpServers: {
|
|
1590
|
+
...defaultConfig.mcpServers || {},
|
|
1591
|
+
...userConfig.mcpServers || {}
|
|
1592
|
+
}
|
|
1593
|
+
};
|
|
1594
|
+
if (Object.keys(mergedConfig.mcpServers).length === 0) {
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
const serverEntries = Object.entries(mergedConfig.mcpServers);
|
|
1598
|
+
for (const [serverName, serverConf] of serverEntries) {
|
|
1599
|
+
try {
|
|
1600
|
+
this.eventBus.emit("backend_message", {
|
|
1601
|
+
type: "connection_status",
|
|
1602
|
+
message: `Connecting to MCP server: ${serverName}...`
|
|
1603
|
+
});
|
|
1604
|
+
if (serverConf.type === "stdio") {
|
|
1605
|
+
await this.connectToStdioServer(serverName, serverConf);
|
|
1606
|
+
} else if (serverConf.type === "sse") {
|
|
1607
|
+
console.warn(`[MCPClient] Conex\xE3o com servidores SSE (como '${serverName}') ainda n\xE3o implementada.`);
|
|
1608
|
+
}
|
|
1609
|
+
} catch (error) {
|
|
1610
|
+
this.eventBus.emit("backend_message", {
|
|
1611
|
+
type: "error",
|
|
1612
|
+
message: `Failed to connect to server '${serverName}'.`
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
async loadMcpConfig(configPath, configType) {
|
|
1618
|
+
try {
|
|
1619
|
+
const fileContent = await fs7.readFile(configPath, "utf-8");
|
|
1620
|
+
const processedContent = this.replaceEnvPlaceholders(fileContent);
|
|
1621
|
+
return JSON.parse(processedContent);
|
|
1622
|
+
} catch (error) {
|
|
1623
|
+
if (error.code === "ENOENT") {
|
|
1624
|
+
if (configType === "User") {
|
|
1625
|
+
}
|
|
1626
|
+
} else {
|
|
1627
|
+
console.warn(`[MCPClient] Warning: Error reading ${configType} config file ${configPath}.`, error);
|
|
1628
|
+
}
|
|
1629
|
+
return {};
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
/**
|
|
1633
|
+
* Conecta-se a um servidor MCP baseado em Stdio, adaptando o comando para o SO atual.
|
|
1634
|
+
*/
|
|
1635
|
+
async connectToStdioServer(serverName, config2) {
|
|
1636
|
+
let commandToExecute = config2.command;
|
|
1637
|
+
let argsToExecute = config2.args || [];
|
|
1638
|
+
const isWindows = os4.platform() === "win32";
|
|
1639
|
+
if (!isWindows && commandToExecute.toLowerCase() === "cmd") {
|
|
1640
|
+
if (argsToExecute.length >= 2 && argsToExecute[0].toLowerCase() === "/c") {
|
|
1641
|
+
commandToExecute = argsToExecute[1];
|
|
1642
|
+
argsToExecute = argsToExecute.slice(2);
|
|
1643
|
+
} else {
|
|
1644
|
+
console.warn(`[MCPClient] Formato de comando 'cmd /c' inesperado para '${serverName}' em sistema n\xE3o-Windows. O servidor ser\xE1 ignorado.`);
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
const transport = new StdioClientTransport({
|
|
1649
|
+
command: commandToExecute,
|
|
1650
|
+
// Usa o comando adaptado
|
|
1651
|
+
args: argsToExecute,
|
|
1652
|
+
// Usa os argumentos adaptados
|
|
1653
|
+
env: config2.env
|
|
1654
|
+
});
|
|
1655
|
+
const mcp = new Client({ name: `bluma-cli-client-for-${serverName}`, version: "1.0.0" });
|
|
1656
|
+
await mcp.connect(transport);
|
|
1657
|
+
this.sessions.set(serverName, mcp);
|
|
1658
|
+
const toolsResult = await mcp.listTools();
|
|
1659
|
+
for (const tool of toolsResult.tools) {
|
|
1660
|
+
const prefixedToolName = `${serverName}_${tool.name}`;
|
|
1661
|
+
this.globalToolsForLlm.push({
|
|
1662
|
+
type: "function",
|
|
1663
|
+
function: {
|
|
1664
|
+
name: prefixedToolName,
|
|
1665
|
+
description: tool.description || "",
|
|
1666
|
+
parameters: tool.inputSchema
|
|
1667
|
+
}
|
|
1668
|
+
});
|
|
1669
|
+
this.toolToServerMap.set(prefixedToolName, {
|
|
1670
|
+
server: serverName,
|
|
1671
|
+
originalName: tool.name
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
async invoke(toolName, args) {
|
|
1676
|
+
const route = this.toolToServerMap.get(toolName);
|
|
1677
|
+
if (!route) {
|
|
1678
|
+
return { error: `Ferramenta '${toolName}' n\xE3o encontrada ou registrada.` };
|
|
1679
|
+
}
|
|
1680
|
+
if (route.server === "native") {
|
|
1681
|
+
return this.nativeToolInvoker.invoke(route.originalName, args);
|
|
1682
|
+
} else {
|
|
1683
|
+
const session = this.sessions.get(route.server);
|
|
1684
|
+
if (!session) {
|
|
1685
|
+
return { error: `Sess\xE3o para o servidor '${route.server}' n\xE3o encontrada.` };
|
|
1686
|
+
}
|
|
1687
|
+
const result = await session.callTool({ name: route.originalName, arguments: args });
|
|
1688
|
+
return result.content;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
getAvailableTools() {
|
|
1692
|
+
return this.globalToolsForLlm;
|
|
1693
|
+
}
|
|
1694
|
+
async close() {
|
|
1695
|
+
for (const [name, session] of this.sessions.entries()) {
|
|
1696
|
+
try {
|
|
1697
|
+
await session.close();
|
|
1698
|
+
} catch (error) {
|
|
1699
|
+
console.error(`[MCPClient] Erro ao encerrar conex\xE3o com '${name}':`, error);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
replaceEnvPlaceholders(content) {
|
|
1704
|
+
return content.replace(/\$\{([A-Za-z0-9_]+)\}/g, (match, varName) => {
|
|
1705
|
+
return process.env[varName] || match;
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
};
|
|
1709
|
+
|
|
1710
|
+
// src/app/agent/feedback/feedback_system.ts
|
|
1711
|
+
var AdvancedFeedbackSystem = class {
|
|
1712
|
+
cumulativeScore = 0;
|
|
1713
|
+
/**
|
|
1714
|
+
* Gera feedback com base em um evento ocorrido.
|
|
1715
|
+
* @param event O evento a ser avaliado.
|
|
1716
|
+
* @returns Um objeto com a pontuação, mensagem e correção.
|
|
1717
|
+
*/
|
|
1718
|
+
generateFeedback(event) {
|
|
1719
|
+
if (event.event === "protocol_violation_direct_text") {
|
|
1720
|
+
const penalty = -2.5;
|
|
1721
|
+
this.cumulativeScore += penalty;
|
|
1722
|
+
return {
|
|
1723
|
+
score: penalty,
|
|
1724
|
+
message: "Direct text response is a protocol violation. All communication must be done via the 'message_notify_dev' tool.",
|
|
1725
|
+
correction: `
|
|
1726
|
+
## PROTOCOL VIOLATION \u2014 SEVERE
|
|
1727
|
+
You sent a direct text response, which is strictly prohibited.
|
|
1728
|
+
PENALTY APPLIED: ${penalty.toFixed(1)} points deducted.
|
|
1729
|
+
You MUST use tools for all actions and communication.
|
|
1730
|
+
`.trim()
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1733
|
+
return { score: 0, message: "No feedback for this event.", correction: "" };
|
|
1734
|
+
}
|
|
1735
|
+
getCumulativeScore() {
|
|
1736
|
+
return this.cumulativeScore;
|
|
1737
|
+
}
|
|
1738
|
+
};
|
|
1739
|
+
|
|
1740
|
+
// src/app/agent/core/context-api/context_manager.ts
|
|
1741
|
+
function createApiContextWindow(fullHistory, maxTurns) {
|
|
1742
|
+
if (!fullHistory.length) {
|
|
1743
|
+
return [];
|
|
1744
|
+
}
|
|
1745
|
+
if (maxTurns === null || maxTurns === void 0) {
|
|
1746
|
+
return [...fullHistory];
|
|
1747
|
+
}
|
|
1748
|
+
const systemMessages = [];
|
|
1749
|
+
let historyStartIndex = 0;
|
|
1750
|
+
while (historyStartIndex < fullHistory.length && fullHistory[historyStartIndex].role === "system") {
|
|
1751
|
+
systemMessages.push(fullHistory[historyStartIndex]);
|
|
1752
|
+
historyStartIndex++;
|
|
1753
|
+
}
|
|
1754
|
+
const conversationHistory = fullHistory.slice(historyStartIndex);
|
|
1755
|
+
const turns = [];
|
|
1756
|
+
let currentTurn = [];
|
|
1757
|
+
let turnsFound = 0;
|
|
1758
|
+
for (let i = conversationHistory.length - 1; i >= 0; i--) {
|
|
1759
|
+
const msg = conversationHistory[i];
|
|
1760
|
+
currentTurn.unshift(msg);
|
|
1761
|
+
if (msg.role === "assistant" && // CORREÇÃO: Adicionamos o tipo explícito para 'tc' para resolver o erro do TypeScript.
|
|
1762
|
+
msg.tool_calls?.some((tc) => tc.function.name === "agent_end_task")) {
|
|
1763
|
+
turns.unshift([...currentTurn]);
|
|
1764
|
+
currentTurn = [];
|
|
1765
|
+
turnsFound++;
|
|
1766
|
+
if (turnsFound >= maxTurns) {
|
|
1767
|
+
break;
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
if (currentTurn.length > 0) {
|
|
1772
|
+
turns.unshift(currentTurn);
|
|
1773
|
+
}
|
|
1774
|
+
const finalContext = systemMessages.concat(turns.flat());
|
|
1775
|
+
return finalContext;
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
// src/app/agent/agent.ts
|
|
1779
|
+
var globalEnvPath = path7.join(os5.homedir(), ".bluma-cli", ".env");
|
|
1780
|
+
dotenv.config({ path: globalEnvPath });
|
|
1781
|
+
var Agent = class {
|
|
1782
|
+
client;
|
|
1783
|
+
deploymentName;
|
|
1784
|
+
sessionId;
|
|
1785
|
+
sessionFile = "";
|
|
1786
|
+
history = [];
|
|
1787
|
+
eventBus;
|
|
1788
|
+
mcpClient;
|
|
1789
|
+
feedbackSystem;
|
|
1790
|
+
isInitialized = false;
|
|
1791
|
+
maxContextTurns = 300;
|
|
1792
|
+
isInterrupted = false;
|
|
1793
|
+
// <-- NOVO: Flag de interrupção
|
|
1794
|
+
constructor(sessionId2, eventBus2) {
|
|
1795
|
+
this.sessionId = sessionId2;
|
|
1796
|
+
this.eventBus = eventBus2;
|
|
1797
|
+
this.eventBus.on("user_interrupt", () => {
|
|
1798
|
+
this.isInterrupted = true;
|
|
1799
|
+
});
|
|
1800
|
+
const nativeToolInvoker = new ToolInvoker();
|
|
1801
|
+
this.mcpClient = new MCPClient(nativeToolInvoker, eventBus2);
|
|
1802
|
+
this.feedbackSystem = new AdvancedFeedbackSystem();
|
|
1803
|
+
const endpoint = process.env.AZURE_OPENAI_ENDPOINT;
|
|
1804
|
+
const apiKey = process.env.AZURE_OPENAI_API_KEY;
|
|
1805
|
+
const apiVersion = process.env.AZURE_OPENAI_API_VERSION;
|
|
1806
|
+
this.deploymentName = process.env.AZURE_OPENAI_DEPLOYMENT || "";
|
|
1807
|
+
if (!endpoint || !apiKey || !apiVersion || !this.deploymentName) {
|
|
1808
|
+
const errorMessage = `Uma ou mais vari\xE1veis de ambiente Azure OpenAI n\xE3o foram encontradas. Verifique em: ${globalEnvPath} ou nas vari\xE1veis de sistema.`;
|
|
1809
|
+
throw new Error(errorMessage);
|
|
1810
|
+
}
|
|
1811
|
+
this.client = new OpenAI({
|
|
1812
|
+
// Configuração do cliente OpenAI hospedado no Azure
|
|
1813
|
+
apiKey,
|
|
1814
|
+
baseURL: `${endpoint}/openai/deployments/${this.deploymentName}`,
|
|
1815
|
+
defaultQuery: { "api-version": apiVersion },
|
|
1816
|
+
defaultHeaders: { "api-key": apiKey }
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
/**
|
|
1820
|
+
* Inicializa o agente, carregando ou criando uma sessão e preparando o histórico.
|
|
1821
|
+
* Também inicializa o MCPClient e o ToolInvoker.
|
|
1822
|
+
*/
|
|
1823
|
+
async initialize() {
|
|
1824
|
+
await this.mcpClient.nativeToolInvoker.initialize();
|
|
1825
|
+
await this.mcpClient.initialize();
|
|
1826
|
+
const [sessionFile, history] = await loadOrcreateSession(this.sessionId);
|
|
1827
|
+
this.sessionFile = sessionFile;
|
|
1828
|
+
this.history = history;
|
|
1829
|
+
if (this.history.length === 0) {
|
|
1830
|
+
let systemPrompt = getUnifiedSystemPrompt();
|
|
1831
|
+
systemPrompt += `
|
|
1832
|
+
BEHAVIORAL REQUIREMENTS:
|
|
1833
|
+
- You MUST use the 'message_notify_dev' tool for ALL communication with the user.
|
|
1834
|
+
- Direct text responses are a protocol violation and will be penalized.
|
|
1835
|
+
- Signal the end of a task using the 'agent_end_task' tool.
|
|
1836
|
+
- Never make parallel tool calls.
|
|
1837
|
+
- Do not include any of the following in tool names:
|
|
1838
|
+
- Special characters
|
|
1839
|
+
- Extra spaces
|
|
1840
|
+
Always use clean, unmodified, and simple names for tools.
|
|
1841
|
+
Tool names must follow strict formatting: no symbols, no whitespace, no alterations.
|
|
1842
|
+
- Follow the stripes o "Tool Naming Policy"
|
|
1843
|
+
- Never modify the names of the tools, use their real names without any modification.
|
|
1844
|
+
- Never forget to signal the system when the task is completed 'agent_end_task' tool.
|
|
1845
|
+
`;
|
|
1846
|
+
this.history.push({ role: "system", content: systemPrompt });
|
|
1847
|
+
await saveSessionHistory(this.sessionFile, this.history);
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
getAvailableTools() {
|
|
1851
|
+
return this.mcpClient.getAvailableTools();
|
|
1852
|
+
}
|
|
1853
|
+
async processTurn(userInput) {
|
|
1854
|
+
this.isInterrupted = false;
|
|
1855
|
+
this.history.push({ role: "user", content: userInput.content });
|
|
1856
|
+
await this._continueConversation();
|
|
1857
|
+
}
|
|
1858
|
+
/**
|
|
1859
|
+
* Lida com a decisão do usuário (aceitar/recusar) sobre uma chamada de ferramenta.
|
|
1860
|
+
* Garante que uma mensagem de 'role: tool' seja sempre adicionada ao histórico.
|
|
1861
|
+
*/
|
|
1862
|
+
async handleToolResponse(decisionData) {
|
|
1863
|
+
const toolCall = decisionData.tool_calls[0];
|
|
1864
|
+
let toolResultContent;
|
|
1865
|
+
let shouldContinueConversation = true;
|
|
1866
|
+
if (decisionData.type === "user_decision_execute") {
|
|
1867
|
+
const toolName = toolCall.function.name;
|
|
1868
|
+
const toolArgs = JSON.parse(toolCall.function.arguments);
|
|
1869
|
+
let previewContent;
|
|
1870
|
+
if (toolName === "edit_tool") {
|
|
1871
|
+
previewContent = await this._generateEditPreview(toolArgs);
|
|
1872
|
+
}
|
|
1873
|
+
this.eventBus.emit("backend_message", {
|
|
1874
|
+
type: "tool_call",
|
|
1875
|
+
tool_name: toolName,
|
|
1876
|
+
arguments: toolArgs,
|
|
1877
|
+
preview: previewContent
|
|
1878
|
+
});
|
|
1879
|
+
try {
|
|
1880
|
+
if (this.isInterrupted) {
|
|
1881
|
+
this.eventBus.emit("backend_message", { type: "info", message: "Agent task cancelled before tool execution." });
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
const result = await this.mcpClient.invoke(toolName, toolArgs);
|
|
1885
|
+
let finalResult = result;
|
|
1886
|
+
if (Array.isArray(result) && result.length > 0 && result[0].type === "text" && typeof result[0].text === "string") {
|
|
1887
|
+
finalResult = result[0].text;
|
|
1888
|
+
}
|
|
1889
|
+
toolResultContent = typeof finalResult === "string" ? finalResult : JSON.stringify(finalResult);
|
|
1890
|
+
} catch (error) {
|
|
1891
|
+
toolResultContent = JSON.stringify({
|
|
1892
|
+
error: `Tool execution failed: ${error.message}`,
|
|
1893
|
+
details: error.data || "No additional details."
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
this.eventBus.emit("backend_message", { type: "tool_result", tool_name: toolName, result: toolResultContent });
|
|
1897
|
+
if (toolName.includes("agent_end_task")) {
|
|
1898
|
+
shouldContinueConversation = false;
|
|
1899
|
+
this.eventBus.emit("backend_message", { type: "done", status: "completed" });
|
|
1900
|
+
}
|
|
1901
|
+
} else {
|
|
1902
|
+
toolResultContent = "The user declined to execute this tool...";
|
|
1903
|
+
}
|
|
1904
|
+
this.history.push({
|
|
1905
|
+
role: "tool",
|
|
1906
|
+
tool_call_id: toolCall.id,
|
|
1907
|
+
content: toolResultContent
|
|
1908
|
+
});
|
|
1909
|
+
await saveSessionHistory(this.sessionFile, this.history);
|
|
1910
|
+
if (shouldContinueConversation && !this.isInterrupted) {
|
|
1911
|
+
await this._continueConversation();
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
/**
|
|
1915
|
+
* Método central que chama a API do LLM e processa a resposta,
|
|
1916
|
+
* com lógica de feedback e auto-aprovação de ferramentas.
|
|
1917
|
+
*/
|
|
1918
|
+
// Adicione este método dentro da classe Agent
|
|
1919
|
+
async _generateEditPreview(toolArgs) {
|
|
1920
|
+
try {
|
|
1921
|
+
const editData = await calculateEdit(toolArgs.file_path, toolArgs.old_string, toolArgs.new_string, toolArgs.expected_replacements || 1);
|
|
1922
|
+
if (editData.error) {
|
|
1923
|
+
return `Failed to generate diff:
|
|
1924
|
+
|
|
1925
|
+
${editData.error.display}`;
|
|
1926
|
+
}
|
|
1927
|
+
const filename = path7.basename(toolArgs.file_path);
|
|
1928
|
+
return createDiff(filename, editData.currentContent || "", editData.newContent);
|
|
1929
|
+
} catch (e) {
|
|
1930
|
+
return `An unexpected error occurred while generating the edit preview: ${e.message}`;
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
async _continueConversation() {
|
|
1934
|
+
try {
|
|
1935
|
+
if (this.isInterrupted) {
|
|
1936
|
+
this.eventBus.emit("backend_message", { type: "info", message: "Agent task cancelled by user." });
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
const contextWindow = createApiContextWindow(this.history, this.maxContextTurns);
|
|
1940
|
+
const response = await this.client.chat.completions.create({
|
|
1941
|
+
model: this.deploymentName,
|
|
1942
|
+
messages: contextWindow,
|
|
1943
|
+
tools: this.mcpClient.getAvailableTools(),
|
|
1944
|
+
tool_choice: "auto",
|
|
1945
|
+
parallel_tool_calls: false
|
|
1946
|
+
});
|
|
1947
|
+
if (this.isInterrupted) {
|
|
1948
|
+
this.eventBus.emit("backend_message", { type: "info", message: "Agent task cancelled by user." });
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
const message = response.choices[0].message;
|
|
1952
|
+
this.history.push(message);
|
|
1953
|
+
if (message.tool_calls) {
|
|
1954
|
+
const autoApprovedTools = [
|
|
1955
|
+
"agent_end_task",
|
|
1956
|
+
"message_notify_dev",
|
|
1957
|
+
"bluma_nootebook"
|
|
1958
|
+
];
|
|
1959
|
+
const toolToCall = message.tool_calls[0];
|
|
1960
|
+
const isSafeTool = autoApprovedTools.some((safeTool) => toolToCall.function.name.includes(safeTool));
|
|
1961
|
+
if (isSafeTool) {
|
|
1962
|
+
await this.handleToolResponse({ type: "user_decision_execute", tool_calls: message.tool_calls });
|
|
1963
|
+
} else {
|
|
1964
|
+
const toolName = toolToCall.function.name;
|
|
1965
|
+
if (toolName === "edit_tool") {
|
|
1966
|
+
const args = JSON.parse(toolToCall.function.arguments);
|
|
1967
|
+
const previewContent = await this._generateEditPreview(args);
|
|
1968
|
+
this.eventBus.emit("backend_message", {
|
|
1969
|
+
type: "confirmation_request",
|
|
1970
|
+
tool_calls: message.tool_calls,
|
|
1971
|
+
preview: previewContent
|
|
1972
|
+
});
|
|
1973
|
+
} else {
|
|
1974
|
+
this.eventBus.emit("backend_message", {
|
|
1975
|
+
type: "confirmation_request",
|
|
1976
|
+
tool_calls: message.tool_calls
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
} else if (message.content) {
|
|
1981
|
+
this.eventBus.emit("backend_message", {
|
|
1982
|
+
type: "assistant_message",
|
|
1983
|
+
content: message.content
|
|
1984
|
+
});
|
|
1985
|
+
const feedback = this.feedbackSystem.generateFeedback({
|
|
1986
|
+
event: "protocol_violation_direct_text",
|
|
1987
|
+
details: { violationContent: message.content }
|
|
1988
|
+
});
|
|
1989
|
+
this.eventBus.emit("backend_message", {
|
|
1990
|
+
type: "protocol_violation",
|
|
1991
|
+
message: feedback.message,
|
|
1992
|
+
content: message.content
|
|
1993
|
+
});
|
|
1994
|
+
this.history.push({
|
|
1995
|
+
role: "system",
|
|
1996
|
+
content: feedback.correction
|
|
1997
|
+
});
|
|
1998
|
+
await this._continueConversation();
|
|
1999
|
+
} else {
|
|
2000
|
+
this.eventBus.emit("backend_message", { type: "info", message: "Agent is thinking... continuing reasoning cycle." });
|
|
2001
|
+
await this._continueConversation();
|
|
2002
|
+
}
|
|
2003
|
+
} catch (error) {
|
|
2004
|
+
const errorMessage = error instanceof Error ? error.message : "An unknown API error occurred.";
|
|
2005
|
+
this.eventBus.emit("backend_message", { type: "error", message: errorMessage });
|
|
2006
|
+
} finally {
|
|
2007
|
+
await saveSessionHistory(this.sessionFile, this.history);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
};
|
|
2011
|
+
|
|
2012
|
+
// src/app/ui/WorkingTimer.tsx
|
|
2013
|
+
import { useState as useState3, useEffect as useEffect2 } from "react";
|
|
2014
|
+
import { Box as Box7, Text as Text7 } from "ink";
|
|
2015
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2016
|
+
var WorkingTimer = () => {
|
|
2017
|
+
const [seconds, setSeconds] = useState3(0);
|
|
2018
|
+
const [dotIndex, setDotIndex] = useState3(1);
|
|
2019
|
+
useEffect2(() => {
|
|
2020
|
+
const secondsTimer = setInterval(() => {
|
|
2021
|
+
setSeconds((prev) => prev + 1);
|
|
2022
|
+
}, 1e3);
|
|
2023
|
+
return () => clearInterval(secondsTimer);
|
|
2024
|
+
}, []);
|
|
2025
|
+
useEffect2(() => {
|
|
2026
|
+
const dotsTimer = setInterval(() => {
|
|
2027
|
+
setDotIndex((prev) => prev % 3 + 1);
|
|
2028
|
+
}, 100);
|
|
2029
|
+
return () => clearInterval(dotsTimer);
|
|
2030
|
+
}, []);
|
|
2031
|
+
const dots = ".".repeat(dotIndex).padEnd(3, " ");
|
|
2032
|
+
return /* @__PURE__ */ jsx7(Box7, { marginBottom: 1, marginTop: 1, paddingX: 1, children: /* @__PURE__ */ jsxs7(Text7, { color: "magenta", children: [
|
|
2033
|
+
`working${dots}`,
|
|
2034
|
+
` ${seconds}s`
|
|
2035
|
+
] }) });
|
|
2036
|
+
};
|
|
2037
|
+
|
|
2038
|
+
// src/app/ui/components/ToolCallDisplay.tsx
|
|
2039
|
+
import { memo as memo2 } from "react";
|
|
2040
|
+
import { Box as Box9 } from "ink";
|
|
2041
|
+
|
|
2042
|
+
// src/app/ui/components/toolCallRenderers.tsx
|
|
2043
|
+
import { Box as Box8, Text as Text8 } from "ink";
|
|
2044
|
+
import path8 from "path";
|
|
2045
|
+
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
2046
|
+
var formatArgumentsForDisplay = (args) => {
|
|
2047
|
+
if (typeof args === "string") {
|
|
2048
|
+
try {
|
|
2049
|
+
return JSON.stringify(JSON.parse(args), null, 2);
|
|
2050
|
+
} catch (e) {
|
|
2051
|
+
return args;
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
return JSON.stringify(args, null, 2);
|
|
2055
|
+
};
|
|
2056
|
+
var renderShellCommand2 = ({ args }) => {
|
|
2057
|
+
const command = args.command || "[command not found]";
|
|
2058
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
2059
|
+
/* @__PURE__ */ jsx8(Box8, { children: /* @__PURE__ */ jsxs8(Text8, { bold: true, children: [
|
|
2060
|
+
/* @__PURE__ */ jsx8(Text8, { color: "green", children: "\u25CF " }),
|
|
2061
|
+
"Shell Command"
|
|
2062
|
+
] }) }),
|
|
2063
|
+
/* @__PURE__ */ jsx8(Box8, { marginLeft: 2, paddingX: 1, children: /* @__PURE__ */ jsxs8(Text8, { children: [
|
|
2064
|
+
/* @__PURE__ */ jsx8(Text8, { color: "gray", children: "\u21B3 " }),
|
|
2065
|
+
/* @__PURE__ */ jsx8(Text8, { color: "cyan", children: command })
|
|
2066
|
+
] }) })
|
|
2067
|
+
] });
|
|
2068
|
+
};
|
|
2069
|
+
var renderLsTool2 = ({ args }) => {
|
|
2070
|
+
let directoryPath = "[path not found]";
|
|
2071
|
+
try {
|
|
2072
|
+
const parsedArgs = typeof args === "string" ? JSON.parse(args) : args;
|
|
2073
|
+
directoryPath = parsedArgs.directory_path || "[path not specified]";
|
|
2074
|
+
} catch (e) {
|
|
2075
|
+
directoryPath = "Error parsing arguments";
|
|
2076
|
+
}
|
|
2077
|
+
const finalDirectoryName = path8.basename(directoryPath);
|
|
2078
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
2079
|
+
/* @__PURE__ */ jsx8(Box8, { children: /* @__PURE__ */ jsxs8(Text8, { bold: true, children: [
|
|
2080
|
+
/* @__PURE__ */ jsx8(Text8, { color: "green", children: "\u25CF " }),
|
|
2081
|
+
"ls Tool"
|
|
2082
|
+
] }) }),
|
|
2083
|
+
/* @__PURE__ */ jsx8(Box8, { marginLeft: 2, paddingX: 1, children: /* @__PURE__ */ jsxs8(Text8, { children: [
|
|
2084
|
+
/* @__PURE__ */ jsx8(Text8, { color: "gray", children: "\u21B3 " }),
|
|
2085
|
+
/* @__PURE__ */ jsx8(Text8, { color: "cyan", children: finalDirectoryName })
|
|
2086
|
+
] }) })
|
|
2087
|
+
] });
|
|
2088
|
+
};
|
|
2089
|
+
var renderCountFilesLines = ({ args }) => {
|
|
2090
|
+
let directoryPath = "[path not found]";
|
|
2091
|
+
try {
|
|
2092
|
+
const parsedArgs = typeof args === "string" ? JSON.parse(args) : args;
|
|
2093
|
+
directoryPath = parsedArgs.filepath || "[path not specified]";
|
|
2094
|
+
} catch (e) {
|
|
2095
|
+
directoryPath = "Error parsing arguments";
|
|
2096
|
+
}
|
|
2097
|
+
const finalDirectoryName = path8.basename(directoryPath);
|
|
2098
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
2099
|
+
/* @__PURE__ */ jsx8(Box8, { children: /* @__PURE__ */ jsxs8(Text8, { bold: true, children: [
|
|
2100
|
+
/* @__PURE__ */ jsx8(Text8, { color: "green", children: "\u25CF " }),
|
|
2101
|
+
"Count File Lines"
|
|
2102
|
+
] }) }),
|
|
2103
|
+
/* @__PURE__ */ jsx8(Box8, { marginLeft: 2, paddingX: 1, children: /* @__PURE__ */ jsxs8(Text8, { children: [
|
|
2104
|
+
/* @__PURE__ */ jsx8(Text8, { color: "gray", children: "\u21B3 " }),
|
|
2105
|
+
/* @__PURE__ */ jsx8(Text8, { color: "cyan", children: finalDirectoryName })
|
|
2106
|
+
] }) })
|
|
2107
|
+
] });
|
|
2108
|
+
};
|
|
2109
|
+
var renderReadFileLines2 = ({ args }) => {
|
|
2110
|
+
let filepath = "[path not found]";
|
|
2111
|
+
let startLine = 0;
|
|
2112
|
+
let endLine = 0;
|
|
2113
|
+
try {
|
|
2114
|
+
const parsedArgs = typeof args === "string" ? JSON.parse(args) : args;
|
|
2115
|
+
filepath = parsedArgs.filepath || "[path not specified]";
|
|
2116
|
+
startLine = parsedArgs.start_line || 0;
|
|
2117
|
+
endLine = parsedArgs.end_line || 0;
|
|
2118
|
+
} catch (e) {
|
|
2119
|
+
filepath = "Error parsing arguments";
|
|
2120
|
+
}
|
|
2121
|
+
const finalFileName = path8.basename(filepath);
|
|
2122
|
+
return (
|
|
2123
|
+
// A caixa externa com a borda, seguindo o template
|
|
2124
|
+
/* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
2125
|
+
/* @__PURE__ */ jsx8(Box8, { children: /* @__PURE__ */ jsxs8(Text8, { bold: true, children: [
|
|
2126
|
+
/* @__PURE__ */ jsx8(Text8, { color: "green", children: "\u25CF " }),
|
|
2127
|
+
"Read File Lines Tool"
|
|
2128
|
+
] }) }),
|
|
2129
|
+
/* @__PURE__ */ jsxs8(Box8, { marginLeft: 2, flexDirection: "column", children: [
|
|
2130
|
+
/* @__PURE__ */ jsx8(Box8, { marginLeft: 2, paddingX: 1, children: /* @__PURE__ */ jsxs8(Text8, { children: [
|
|
2131
|
+
/* @__PURE__ */ jsx8(Text8, { color: "gray", children: "\u21B3 " }),
|
|
2132
|
+
/* @__PURE__ */ jsx8(Text8, { color: "cyan", children: finalFileName })
|
|
2133
|
+
] }) }),
|
|
2134
|
+
/* @__PURE__ */ jsx8(Box8, { marginLeft: 2, paddingX: 4, children: /* @__PURE__ */ jsxs8(Text8, { children: [
|
|
2135
|
+
/* @__PURE__ */ jsx8(Text8, { color: "gray", children: "\u21B3 " }),
|
|
2136
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "lines " }),
|
|
2137
|
+
/* @__PURE__ */ jsx8(Text8, { color: "cyan", children: startLine }),
|
|
2138
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " to " }),
|
|
2139
|
+
/* @__PURE__ */ jsx8(Text8, { color: "cyan", children: endLine })
|
|
2140
|
+
] }) })
|
|
2141
|
+
] })
|
|
2142
|
+
] })
|
|
2143
|
+
);
|
|
2144
|
+
};
|
|
2145
|
+
var renderBlumaNotebook = ({ args }) => {
|
|
2146
|
+
try {
|
|
2147
|
+
let dataToParse = args;
|
|
2148
|
+
if (args && typeof args === "object") {
|
|
2149
|
+
if (args.content) dataToParse = args.content;
|
|
2150
|
+
else if (args.data) dataToParse = args.data;
|
|
2151
|
+
}
|
|
2152
|
+
const thinkingData = typeof dataToParse === "string" ? JSON.parse(dataToParse) : dataToParse;
|
|
2153
|
+
if (!thinkingData || typeof thinkingData.thought !== "string") {
|
|
2154
|
+
throw new Error("Invalid or missing 'thought' property.");
|
|
2155
|
+
}
|
|
2156
|
+
return (
|
|
2157
|
+
// Usamos a mesma estrutura de caixa com borda
|
|
2158
|
+
/* @__PURE__ */ jsxs8(Box8, { borderStyle: "round", borderColor: "green", flexDirection: "column", paddingX: 1, children: [
|
|
2159
|
+
/* @__PURE__ */ jsx8(Box8, { children: /* @__PURE__ */ jsxs8(Text8, { bold: true, children: [
|
|
2160
|
+
/* @__PURE__ */ jsx8(Text8, { color: "green", children: "\u{1F9E0} " }),
|
|
2161
|
+
"Thinking Process"
|
|
2162
|
+
] }) }),
|
|
2163
|
+
/* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexDirection: "column", children: [
|
|
2164
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Thought:" }),
|
|
2165
|
+
/* @__PURE__ */ jsx8(Box8, { marginLeft: 2, children: /* @__PURE__ */ jsx8(Text8, { color: "gray", children: thinkingData.thought }) })
|
|
2166
|
+
] }),
|
|
2167
|
+
thinkingData.remaining_tasks && thinkingData.remaining_tasks.length > 0 && /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexDirection: "column", children: [
|
|
2168
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Remaining Tasks:" }),
|
|
2169
|
+
thinkingData.remaining_tasks.map((task, index) => /* @__PURE__ */ jsx8(Box8, { marginLeft: 2, children: /* @__PURE__ */ jsxs8(Text8, { children: [
|
|
2170
|
+
/* @__PURE__ */ jsx8(Text8, { color: "gray", children: "\u21B3 " }),
|
|
2171
|
+
/* @__PURE__ */ jsx8(Text8, { color: task.startsWith("\u{1F5F8}") ? "green" : "yellow", children: task })
|
|
2172
|
+
] }) }, index))
|
|
2173
|
+
] })
|
|
2174
|
+
] })
|
|
2175
|
+
);
|
|
2176
|
+
} catch (e) {
|
|
2177
|
+
return /* @__PURE__ */ jsxs8(Box8, { borderStyle: "round", borderColor: "blue", paddingX: 1, children: [
|
|
2178
|
+
/* @__PURE__ */ jsx8(Text8, { color: "blue", bold: true, children: "Thinking (Error)" }),
|
|
2179
|
+
/* @__PURE__ */ jsx8(Text8, { color: "gray", children: JSON.stringify(args, null, 2) })
|
|
2180
|
+
] });
|
|
2181
|
+
}
|
|
2182
|
+
};
|
|
2183
|
+
var renderEditToolCall = ({ args, preview }) => {
|
|
2184
|
+
let filepath = "[path not specified]";
|
|
2185
|
+
try {
|
|
2186
|
+
const parsedArgs = typeof args === "string" ? JSON.parse(args) : args;
|
|
2187
|
+
filepath = parsedArgs.file_path || "[path not specified]";
|
|
2188
|
+
} catch (e) {
|
|
2189
|
+
filepath = "Error parsing arguments";
|
|
2190
|
+
}
|
|
2191
|
+
const finalFileName = path8.basename(filepath);
|
|
2192
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", paddingX: 1, children: [
|
|
2193
|
+
/* @__PURE__ */ jsx8(Box8, { children: /* @__PURE__ */ jsxs8(Text8, { bold: true, children: [
|
|
2194
|
+
/* @__PURE__ */ jsx8(Text8, { color: "green", children: "\u25CF " }),
|
|
2195
|
+
"Edit File"
|
|
2196
|
+
] }) }),
|
|
2197
|
+
/* @__PURE__ */ jsx8(Box8, { marginLeft: 2, paddingX: 1, children: /* @__PURE__ */ jsxs8(Text8, { children: [
|
|
2198
|
+
/* @__PURE__ */ jsx8(Text8, { color: "gray", children: "\u21B3 " }),
|
|
2199
|
+
/* @__PURE__ */ jsx8(Text8, { color: "cyan", children: finalFileName })
|
|
2200
|
+
] }) }),
|
|
2201
|
+
preview && /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(SimpleDiff, { text: preview, maxHeight: Infinity }) })
|
|
2202
|
+
] });
|
|
2203
|
+
};
|
|
2204
|
+
var renderGenericToolCall = ({ toolName, args }) => {
|
|
2205
|
+
const formattedArgs = formatArgumentsForDisplay(args);
|
|
2206
|
+
return (
|
|
2207
|
+
// A "moldura" padrão de sucesso com a borda cinza
|
|
2208
|
+
/* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [
|
|
2209
|
+
/* @__PURE__ */ jsx8(Box8, { children: /* @__PURE__ */ jsxs8(Text8, { bold: true, children: [
|
|
2210
|
+
/* @__PURE__ */ jsx8(Text8, { color: "green", children: "\u25CF " }),
|
|
2211
|
+
toolName
|
|
2212
|
+
] }) }),
|
|
2213
|
+
formattedArgs && formattedArgs !== "{}" && /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexDirection: "column", children: [
|
|
2214
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Arguments passed:" }),
|
|
2215
|
+
/* @__PURE__ */ jsx8(Box8, { marginLeft: 2, flexDirection: "column", children: formattedArgs.split("\n").map((line, index) => /* @__PURE__ */ jsxs8(Text8, { color: "gray", children: [
|
|
2216
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u21B3 " }),
|
|
2217
|
+
line
|
|
2218
|
+
] }, index)) })
|
|
2219
|
+
] })
|
|
2220
|
+
] })
|
|
2221
|
+
);
|
|
2222
|
+
};
|
|
2223
|
+
var ToolRenderDisplay = {
|
|
2224
|
+
"shell_command": renderShellCommand2,
|
|
2225
|
+
"ls_tool": renderLsTool2,
|
|
2226
|
+
"bluma_nootebook": renderBlumaNotebook,
|
|
2227
|
+
"count_file_lines": renderCountFilesLines,
|
|
2228
|
+
"read_file_lines": renderReadFileLines2,
|
|
2229
|
+
"edit_tool": renderEditToolCall
|
|
2230
|
+
};
|
|
2231
|
+
|
|
2232
|
+
// src/app/ui/components/ToolCallDisplay.tsx
|
|
2233
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
2234
|
+
var ToolCallDisplayComponent = ({ toolName, args, preview }) => {
|
|
2235
|
+
if (toolName.includes("message_notify_dev") || toolName.includes("agent_end_task")) {
|
|
2236
|
+
return null;
|
|
2237
|
+
}
|
|
2238
|
+
const Renderer = ToolRenderDisplay[toolName] || renderGenericToolCall;
|
|
2239
|
+
return /* @__PURE__ */ jsx9(Box9, { marginBottom: 1, children: /* @__PURE__ */ jsx9(Renderer, { toolName, args, preview }) });
|
|
2240
|
+
};
|
|
2241
|
+
var ToolCallDisplay = memo2(ToolCallDisplayComponent);
|
|
2242
|
+
|
|
2243
|
+
// src/app/ui/components/ToolResultDisplay.tsx
|
|
2244
|
+
import { memo as memo3 } from "react";
|
|
2245
|
+
import { Box as Box10, Text as Text9 } from "ink";
|
|
2246
|
+
import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2247
|
+
var ToolResultDisplayComponent = ({ toolName, result }) => {
|
|
2248
|
+
const MAX_LINES = 3;
|
|
2249
|
+
if (toolName.includes("agent_end_task") || toolName.includes("bluma_nootebook") || toolName.includes("shell_command") || toolName.includes("ls_tool") || toolName.includes("count_file_lines") || toolName.includes("read_file_lines") || toolName.includes("edit_tool")) {
|
|
2250
|
+
return null;
|
|
2251
|
+
}
|
|
2252
|
+
if (toolName.includes("message_notify_dev")) {
|
|
2253
|
+
try {
|
|
2254
|
+
const parsed = JSON.parse(result);
|
|
2255
|
+
if (parsed.content && parsed.content.body) {
|
|
2256
|
+
const bodyText = parsed.content.body.trim();
|
|
2257
|
+
return /* @__PURE__ */ jsx10(Box10, { marginBottom: 1, paddingX: 1, children: /* @__PURE__ */ jsx10(Text9, { children: bodyText }) });
|
|
2258
|
+
}
|
|
2259
|
+
} catch (e) {
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
let formattedResult = result;
|
|
2263
|
+
try {
|
|
2264
|
+
const parsedJson = JSON.parse(result);
|
|
2265
|
+
formattedResult = JSON.stringify(parsedJson, null, 2);
|
|
2266
|
+
} catch (e) {
|
|
2267
|
+
formattedResult = result;
|
|
2268
|
+
}
|
|
2269
|
+
const lines = formattedResult.split("\n");
|
|
2270
|
+
const isTruncated = lines.length > MAX_LINES;
|
|
2271
|
+
const visibleLines = isTruncated ? lines.slice(0, MAX_LINES) : lines;
|
|
2272
|
+
const remainingCount = lines.length - MAX_LINES;
|
|
2273
|
+
return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", marginBottom: 1, children: [
|
|
2274
|
+
visibleLines.map((line, idx) => /* @__PURE__ */ jsx10(Text9, { color: "gray", children: line }, idx)),
|
|
2275
|
+
isTruncated && /* @__PURE__ */ jsxs9(Text9, { color: "gray", children: [
|
|
2276
|
+
"...(",
|
|
2277
|
+
remainingCount,
|
|
2278
|
+
" more lines)"
|
|
2279
|
+
] })
|
|
2280
|
+
] });
|
|
2281
|
+
};
|
|
2282
|
+
var ToolResultDisplay = memo3(ToolResultDisplayComponent);
|
|
2283
|
+
|
|
2284
|
+
// src/app/ui/App.tsx
|
|
2285
|
+
import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
2286
|
+
var AppComponent = ({ eventBus: eventBus2, sessionId: sessionId2 }) => {
|
|
2287
|
+
const agentInstance = useRef(null);
|
|
2288
|
+
const [history, setHistory] = useState4([]);
|
|
2289
|
+
const [statusMessage, setStatusMessage] = useState4(
|
|
2290
|
+
"Initializing agent..."
|
|
2291
|
+
);
|
|
2292
|
+
const [toolsCount, setToolsCount] = useState4(null);
|
|
2293
|
+
const [mcpStatus, setMcpStatus] = useState4(
|
|
2294
|
+
"connecting"
|
|
2295
|
+
);
|
|
2296
|
+
const [isProcessing, setIsProcessing] = useState4(true);
|
|
2297
|
+
const [pendingConfirmation, setPendingConfirmation] = useState4(
|
|
2298
|
+
null
|
|
2299
|
+
);
|
|
2300
|
+
const [confirmationPreview, setConfirmationPreview] = useState4(null);
|
|
2301
|
+
const alwaysAcceptList = useRef([]);
|
|
2302
|
+
const workdir = process.cwd();
|
|
2303
|
+
const handleInterrupt = useCallback(() => {
|
|
2304
|
+
if (!isProcessing) return;
|
|
2305
|
+
eventBus2.emit("user_interrupt");
|
|
2306
|
+
setIsProcessing(false);
|
|
2307
|
+
setHistory((prev) => [
|
|
2308
|
+
...prev,
|
|
2309
|
+
{
|
|
2310
|
+
id: prev.length,
|
|
2311
|
+
component: /* @__PURE__ */ jsx11(Text10, { color: "yellow", children: "-- Task cancelled by dev. --" })
|
|
2312
|
+
}
|
|
2313
|
+
]);
|
|
2314
|
+
}, [isProcessing, eventBus2]);
|
|
2315
|
+
const handleSubmit = useCallback(
|
|
2316
|
+
(text) => {
|
|
2317
|
+
if (!text || isProcessing || !agentInstance.current) return;
|
|
2318
|
+
setIsProcessing(true);
|
|
2319
|
+
const displayText = text.length > 1e4 ? text.substring(0, 1e4) + "..." : text;
|
|
2320
|
+
setHistory((prev) => [
|
|
2321
|
+
...prev,
|
|
2322
|
+
{
|
|
2323
|
+
id: prev.length,
|
|
2324
|
+
component: (
|
|
2325
|
+
// Uma única Box para o espaçamento
|
|
2326
|
+
/* @__PURE__ */ jsx11(Box11, { marginBottom: 1, children: /* @__PURE__ */ jsxs10(Text10, { color: "white", dimColor: true, children: [
|
|
2327
|
+
/* @__PURE__ */ jsxs10(Text10, { color: "white", children: [
|
|
2328
|
+
">",
|
|
2329
|
+
" "
|
|
2330
|
+
] }),
|
|
2331
|
+
displayText
|
|
2332
|
+
] }) })
|
|
2333
|
+
)
|
|
2334
|
+
}
|
|
2335
|
+
]);
|
|
2336
|
+
agentInstance.current.processTurn({ content: text });
|
|
2337
|
+
},
|
|
2338
|
+
[isProcessing]
|
|
2339
|
+
);
|
|
2340
|
+
const handleConfirmation = useCallback(
|
|
2341
|
+
(decision, toolCalls) => {
|
|
2342
|
+
if (!agentInstance.current) return;
|
|
2343
|
+
setPendingConfirmation(null);
|
|
2344
|
+
setIsProcessing(true);
|
|
2345
|
+
let finalDecision = decision;
|
|
2346
|
+
if (decision === "accept_always") {
|
|
2347
|
+
const toolNameToWhitelist = toolCalls[0].function.name;
|
|
2348
|
+
if (!alwaysAcceptList.current.includes(toolNameToWhitelist)) {
|
|
2349
|
+
alwaysAcceptList.current.push(toolNameToWhitelist);
|
|
2350
|
+
}
|
|
2351
|
+
finalDecision = "accept";
|
|
2352
|
+
}
|
|
2353
|
+
const messageType = finalDecision === "accept" ? "user_decision_execute" : "user_decision_decline";
|
|
2354
|
+
agentInstance.current.handleToolResponse({
|
|
2355
|
+
type: messageType,
|
|
2356
|
+
tool_calls: toolCalls
|
|
2357
|
+
});
|
|
2358
|
+
},
|
|
2359
|
+
[]
|
|
2360
|
+
);
|
|
2361
|
+
useEffect3(() => {
|
|
2362
|
+
setHistory([{ id: 0, component: /* @__PURE__ */ jsx11(Header, {}) }]);
|
|
2363
|
+
const initializeAgent = async () => {
|
|
2364
|
+
try {
|
|
2365
|
+
agentInstance.current = new Agent(sessionId2, eventBus2);
|
|
2366
|
+
await agentInstance.current.initialize();
|
|
2367
|
+
eventBus2.emit("backend_message", {
|
|
2368
|
+
type: "status",
|
|
2369
|
+
status: "mcp_connected",
|
|
2370
|
+
tools: agentInstance.current.getAvailableTools().length
|
|
2371
|
+
});
|
|
2372
|
+
} catch (error) {
|
|
2373
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error during Agent initialization.";
|
|
2374
|
+
eventBus2.emit("backend_message", {
|
|
2375
|
+
type: "error",
|
|
2376
|
+
message: errorMessage
|
|
2377
|
+
});
|
|
2378
|
+
}
|
|
2379
|
+
};
|
|
2380
|
+
const handleBackendMessage = (parsed) => {
|
|
2381
|
+
try {
|
|
2382
|
+
if (parsed.type === "connection_status") {
|
|
2383
|
+
setStatusMessage(parsed.message);
|
|
2384
|
+
return;
|
|
2385
|
+
}
|
|
2386
|
+
if (parsed.type === "confirmation_request") {
|
|
2387
|
+
const toolToConfirm = parsed.tool_calls[0].function.name;
|
|
2388
|
+
if (alwaysAcceptList.current.includes(toolToConfirm)) {
|
|
2389
|
+
handleConfirmation("accept", parsed.tool_calls);
|
|
2390
|
+
return;
|
|
2391
|
+
}
|
|
2392
|
+
setPendingConfirmation(parsed.tool_calls);
|
|
2393
|
+
setConfirmationPreview(parsed.preview || null);
|
|
2394
|
+
setIsProcessing(false);
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
2397
|
+
if (parsed.type === "done") {
|
|
2398
|
+
if (parsed.status !== "awaiting_confirmation") {
|
|
2399
|
+
setStatusMessage(null);
|
|
2400
|
+
}
|
|
2401
|
+
setIsProcessing(false);
|
|
2402
|
+
return;
|
|
2403
|
+
}
|
|
2404
|
+
if (parsed.type === "status" && parsed.status === "mcp_connected") {
|
|
2405
|
+
setStatusMessage(null);
|
|
2406
|
+
setToolsCount(parsed.tools);
|
|
2407
|
+
setMcpStatus("connected");
|
|
2408
|
+
setIsProcessing(false);
|
|
2409
|
+
setHistory((prev) => {
|
|
2410
|
+
const newHistory = [...prev];
|
|
2411
|
+
if (prev.length < 2) {
|
|
2412
|
+
newHistory.push({
|
|
2413
|
+
id: 1,
|
|
2414
|
+
component: /* @__PURE__ */ jsx11(
|
|
2415
|
+
SessionInfo,
|
|
2416
|
+
{
|
|
2417
|
+
sessionId: sessionId2,
|
|
2418
|
+
toolsCount: parsed.tools,
|
|
2419
|
+
mcpStatus: "connected",
|
|
2420
|
+
workdir
|
|
2421
|
+
}
|
|
2422
|
+
)
|
|
2423
|
+
});
|
|
2424
|
+
}
|
|
2425
|
+
return newHistory;
|
|
2426
|
+
});
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2429
|
+
if (parsed.type === "error") {
|
|
2430
|
+
setStatusMessage(null);
|
|
2431
|
+
setIsProcessing(false);
|
|
2432
|
+
}
|
|
2433
|
+
let newComponent = null;
|
|
2434
|
+
if (parsed.type === "debug") {
|
|
2435
|
+
newComponent = /* @__PURE__ */ jsx11(Text10, { color: "gray", children: parsed.message });
|
|
2436
|
+
} else if (parsed.type === "protocol_violation") {
|
|
2437
|
+
newComponent = /* @__PURE__ */ jsxs10(
|
|
2438
|
+
Box11,
|
|
2439
|
+
{
|
|
2440
|
+
borderStyle: "round",
|
|
2441
|
+
borderColor: "yellow",
|
|
2442
|
+
flexDirection: "column",
|
|
2443
|
+
marginBottom: 1,
|
|
2444
|
+
paddingX: 1,
|
|
2445
|
+
children: [
|
|
2446
|
+
" ",
|
|
2447
|
+
/* @__PURE__ */ jsxs10(Text10, { color: "yellow", bold: true, children: [
|
|
2448
|
+
" ",
|
|
2449
|
+
"Protocol Violation",
|
|
2450
|
+
" "
|
|
2451
|
+
] }),
|
|
2452
|
+
" ",
|
|
2453
|
+
/* @__PURE__ */ jsx11(Text10, { color: "gray", children: parsed.content }),
|
|
2454
|
+
" ",
|
|
2455
|
+
/* @__PURE__ */ jsx11(Text10, { color: "yellow", children: parsed.message }),
|
|
2456
|
+
" "
|
|
2457
|
+
]
|
|
2458
|
+
}
|
|
2459
|
+
);
|
|
2460
|
+
} else if (parsed.type === "error") {
|
|
2461
|
+
newComponent = /* @__PURE__ */ jsxs10(Text10, { color: "red", children: [
|
|
2462
|
+
"\u274C ",
|
|
2463
|
+
parsed.message
|
|
2464
|
+
] });
|
|
2465
|
+
} else if (parsed.type === "tool_call") {
|
|
2466
|
+
newComponent = /* @__PURE__ */ jsx11(
|
|
2467
|
+
ToolCallDisplay,
|
|
2468
|
+
{
|
|
2469
|
+
toolName: parsed.tool_name,
|
|
2470
|
+
args: parsed.arguments,
|
|
2471
|
+
preview: parsed.preview
|
|
2472
|
+
}
|
|
2473
|
+
);
|
|
2474
|
+
} else if (parsed.type === "tool_result") {
|
|
2475
|
+
newComponent = /* @__PURE__ */ jsx11(
|
|
2476
|
+
ToolResultDisplay,
|
|
2477
|
+
{
|
|
2478
|
+
toolName: parsed.tool_name,
|
|
2479
|
+
result: parsed.result
|
|
2480
|
+
}
|
|
2481
|
+
);
|
|
2482
|
+
} else if (parsed.type === "assistant_message" && parsed.content) {
|
|
2483
|
+
newComponent = /* @__PURE__ */ jsx11(Box11, { paddingX: 1, marginBottom: 1, children: /* @__PURE__ */ jsx11(Text10, { color: "blue", children: parsed.content }) });
|
|
2484
|
+
}
|
|
2485
|
+
if (newComponent) {
|
|
2486
|
+
setHistory((prev) => [
|
|
2487
|
+
...prev,
|
|
2488
|
+
{ id: prev.length, component: newComponent }
|
|
2489
|
+
]);
|
|
2490
|
+
}
|
|
2491
|
+
} catch (error) {
|
|
2492
|
+
}
|
|
2493
|
+
};
|
|
2494
|
+
eventBus2.on("backend_message", handleBackendMessage);
|
|
2495
|
+
initializeAgent();
|
|
2496
|
+
return () => {
|
|
2497
|
+
eventBus2.off("backend_message", handleBackendMessage);
|
|
2498
|
+
};
|
|
2499
|
+
}, [eventBus2, sessionId2, handleConfirmation]);
|
|
2500
|
+
const renderInteractiveComponent = () => {
|
|
2501
|
+
if (mcpStatus !== "connected") {
|
|
2502
|
+
return /* @__PURE__ */ jsx11(Box11, { borderStyle: "round", borderColor: "black", children: /* @__PURE__ */ jsxs10(Text10, { color: "yellow", children: [
|
|
2503
|
+
/* @__PURE__ */ jsx11(Spinner, { type: "dots" }),
|
|
2504
|
+
" ",
|
|
2505
|
+
statusMessage || "Connecting..."
|
|
2506
|
+
] }) });
|
|
2507
|
+
}
|
|
2508
|
+
if (pendingConfirmation) {
|
|
2509
|
+
return /* @__PURE__ */ jsx11(
|
|
2510
|
+
ConfirmationPrompt,
|
|
2511
|
+
{
|
|
2512
|
+
toolCalls: pendingConfirmation,
|
|
2513
|
+
preview: confirmationPreview,
|
|
2514
|
+
onDecision: (decision) => {
|
|
2515
|
+
setConfirmationPreview(null);
|
|
2516
|
+
handleConfirmation(decision, pendingConfirmation);
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
);
|
|
2520
|
+
}
|
|
2521
|
+
return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", children: [
|
|
2522
|
+
isProcessing && !pendingConfirmation && /* @__PURE__ */ jsx11(WorkingTimer, {}),
|
|
2523
|
+
/* @__PURE__ */ jsx11(
|
|
2524
|
+
InputPrompt,
|
|
2525
|
+
{
|
|
2526
|
+
onSubmit: handleSubmit,
|
|
2527
|
+
isReadOnly: isProcessing,
|
|
2528
|
+
onInterrupt: handleInterrupt
|
|
2529
|
+
}
|
|
2530
|
+
)
|
|
2531
|
+
] });
|
|
2532
|
+
};
|
|
2533
|
+
return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", children: [
|
|
2534
|
+
/* @__PURE__ */ jsx11(Static, { items: history, children: (item) => /* @__PURE__ */ jsx11(Box11, { children: item.component }, item.id) }),
|
|
2535
|
+
renderInteractiveComponent()
|
|
2536
|
+
] });
|
|
2537
|
+
};
|
|
2538
|
+
var App = memo4(AppComponent);
|
|
2539
|
+
var App_default = App;
|
|
2540
|
+
|
|
2541
|
+
// src/main.ts
|
|
2542
|
+
var eventBus = new EventEmitter();
|
|
2543
|
+
var sessionId = uuidv42();
|
|
2544
|
+
var props = {
|
|
2545
|
+
eventBus,
|
|
2546
|
+
sessionId
|
|
2547
|
+
};
|
|
2548
|
+
render(React6.createElement(App_default, props));
|