@nomad-e/bluma-cli 0.0.107 → 0.0.109

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.js +2293 -2206
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -1,1266 +1,1588 @@
1
1
  #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
2
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
3
4
  var __esm = (fn, res) => function __init() {
4
5
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
6
  };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
6
11
 
7
- // src/app/agent/core/llm/tool_call_normalizer.ts
8
- import { randomUUID } from "crypto";
9
- var ToolCallNormalizer;
10
- var init_tool_call_normalizer = __esm({
11
- "src/app/agent/core/llm/tool_call_normalizer.ts"() {
12
- "use strict";
13
- ToolCallNormalizer = class {
14
- /**
15
- * Normaliza a mensagem do assistant, convertendo diferentes formatos de tool calls
16
- */
17
- static normalizeAssistantMessage(message) {
18
- if (message.tool_calls && this.isOpenAIFormat(message.tool_calls)) {
19
- return message;
20
- }
21
- const toolCalls = this.extractToolCalls(message);
22
- if (toolCalls.length > 0) {
23
- return {
24
- role: message.role || "assistant",
25
- content: message.content || null,
26
- tool_calls: toolCalls
27
- };
28
- }
29
- return message;
30
- }
31
- /**
32
- * Verifica se já está no formato OpenAI
33
- */
34
- static isOpenAIFormat(toolCalls) {
35
- if (!Array.isArray(toolCalls) || toolCalls.length === 0) return false;
36
- const firstCall = toolCalls[0];
37
- return typeof firstCall.id === "string" && firstCall.type === "function" && typeof firstCall.function?.name === "string" && typeof firstCall.function?.arguments === "string";
38
- }
39
- /**
40
- * Extrai tool calls de diversos formatos possíveis
41
- */
42
- static extractToolCalls(message) {
43
- const results = [];
44
- if (message.tool_calls && Array.isArray(message.tool_calls)) {
45
- for (const call of message.tool_calls) {
46
- const normalized = this.normalizeToolCall(call);
47
- if (normalized) results.push(normalized);
48
- }
49
- }
50
- if (typeof message.content === "string" && message.content.trim()) {
51
- const extracted = this.extractFromContent(message.content);
52
- results.push(...extracted);
53
- }
54
- if (message.function_call) {
55
- const normalized = this.normalizeToolCall(message.function_call);
56
- if (normalized) results.push(normalized);
57
- }
58
- return results;
59
- }
60
- /**
61
- * Normaliza um único tool call para o formato OpenAI
62
- */
63
- static normalizeToolCall(call) {
64
- try {
65
- if (call.id && call.function?.name) {
66
- return {
67
- id: call.id,
68
- type: "function",
69
- function: {
70
- name: call.function.name,
71
- arguments: typeof call.function.arguments === "string" ? call.function.arguments : JSON.stringify(call.function.arguments)
72
- }
73
- };
74
- }
75
- if (call.name) {
76
- return {
77
- id: call.id || randomUUID(),
78
- type: "function",
79
- function: {
80
- name: call.name,
81
- arguments: typeof call.arguments === "string" ? call.arguments : JSON.stringify(call.arguments || {})
82
- }
83
- };
84
- }
85
- if (call.function && typeof call.function === "object") {
86
- return {
87
- id: call.id || randomUUID(),
88
- type: "function",
89
- function: {
90
- name: call.function.name,
91
- arguments: typeof call.function.arguments === "string" ? call.function.arguments : JSON.stringify(call.function.arguments || {})
92
- }
93
- };
94
- }
95
- return null;
96
- } catch (error) {
97
- console.error("Error normalizing tool call:", error, call);
98
- return null;
12
+ // src/app/agent/tools/natives/async_command.ts
13
+ var async_command_exports = {};
14
+ __export(async_command_exports, {
15
+ commandStatus: () => commandStatus,
16
+ killCommand: () => killCommand,
17
+ runCommandAsync: () => runCommandAsync,
18
+ sendCommandInput: () => sendCommandInput
19
+ });
20
+ import os2 from "os";
21
+ import { spawn } from "child_process";
22
+ import { v4 as uuidv42 } from "uuid";
23
+ function cleanupOldCommands() {
24
+ if (runningCommands.size <= MAX_STORED_COMMANDS) return;
25
+ const commands = Array.from(runningCommands.entries()).filter(([_, cmd]) => cmd.status !== "running").sort((a, b) => (a[1].endTime || 0) - (b[1].endTime || 0));
26
+ const toRemove = commands.slice(0, commands.length - MAX_STORED_COMMANDS + 10);
27
+ for (const [id] of toRemove) {
28
+ runningCommands.delete(id);
29
+ }
30
+ }
31
+ function isDangerousCommand(command) {
32
+ const trimmed = command.trim();
33
+ for (const pattern of DANGEROUS_PATTERNS) {
34
+ if (pattern.test(trimmed)) {
35
+ return "Command requires elevated privileges (sudo/doas) which cannot be handled in async mode";
36
+ }
37
+ }
38
+ for (const pattern of INTERACTIVE_PATTERNS) {
39
+ if (pattern.test(trimmed)) {
40
+ return "Interactive commands are not supported in async mode";
41
+ }
42
+ }
43
+ return null;
44
+ }
45
+ async function runCommandAsync(args) {
46
+ try {
47
+ const {
48
+ command,
49
+ cwd = process.cwd(),
50
+ timeout = 0
51
+ } = args;
52
+ if (!command || typeof command !== "string") {
53
+ return {
54
+ success: false,
55
+ error: "command is required and must be a string"
56
+ };
57
+ }
58
+ const dangerReason = isDangerousCommand(command);
59
+ if (dangerReason) {
60
+ return {
61
+ success: false,
62
+ error: dangerReason
63
+ };
64
+ }
65
+ const commandId = uuidv42().substring(0, 8);
66
+ const platform = os2.platform();
67
+ let shellCmd;
68
+ let shellArgs;
69
+ if (platform === "win32") {
70
+ shellCmd = process.env.COMSPEC || "cmd.exe";
71
+ shellArgs = ["/c", command];
72
+ } else {
73
+ shellCmd = process.env.SHELL || "/bin/bash";
74
+ shellArgs = ["-c", command];
75
+ }
76
+ const entry = {
77
+ id: commandId,
78
+ command,
79
+ status: "running",
80
+ stdout: "",
81
+ stderr: "",
82
+ exitCode: null,
83
+ startTime: Date.now(),
84
+ process: null
85
+ };
86
+ const child = spawn(shellCmd, shellArgs, {
87
+ cwd,
88
+ env: process.env,
89
+ windowsHide: true
90
+ });
91
+ entry.process = child;
92
+ let stdoutTruncated = false;
93
+ child.stdout?.on("data", (data) => {
94
+ if (!stdoutTruncated) {
95
+ entry.stdout += data.toString();
96
+ if (entry.stdout.length > MAX_OUTPUT_SIZE) {
97
+ entry.stdout = entry.stdout.substring(0, MAX_OUTPUT_SIZE) + OUTPUT_TRUNCATION_MSG;
98
+ stdoutTruncated = true;
99
99
  }
100
100
  }
101
- /**
102
- * Extrai tool calls do content (pode estar em markdown, JSON, etc)
103
- */
104
- static extractFromContent(content) {
105
- const results = [];
106
- const cleanContent = content.replace(/```(?:json)?\s*([\s\S]*?)```/g, "$1");
107
- const jsonMatches = this.extractJsonObjects(cleanContent);
108
- for (const jsonStr of jsonMatches) {
109
- try {
110
- const parsed = JSON.parse(jsonStr);
111
- if (Array.isArray(parsed)) {
112
- for (const call of parsed) {
113
- const normalized = this.normalizeToolCall(call);
114
- if (normalized) results.push(normalized);
115
- }
116
- } else if (parsed.name || parsed.function) {
117
- const normalized = this.normalizeToolCall(parsed);
118
- if (normalized) results.push(normalized);
119
- } else if (parsed.tool_calls && Array.isArray(parsed.tool_calls)) {
120
- for (const call of parsed.tool_calls) {
121
- const normalized = this.normalizeToolCall(call);
122
- if (normalized) results.push(normalized);
123
- }
124
- }
125
- } catch (e) {
126
- }
101
+ });
102
+ let stderrTruncated = false;
103
+ child.stderr?.on("data", (data) => {
104
+ if (!stderrTruncated) {
105
+ entry.stderr += data.toString();
106
+ if (entry.stderr.length > MAX_OUTPUT_SIZE) {
107
+ entry.stderr = entry.stderr.substring(0, MAX_OUTPUT_SIZE) + OUTPUT_TRUNCATION_MSG;
108
+ stderrTruncated = true;
127
109
  }
128
- return results;
129
110
  }
130
- /**
131
- * Extrai objetos JSON de uma string (suporta múltiplos objetos)
132
- */
133
- static extractJsonObjects(text) {
134
- const results = [];
135
- let depth = 0;
136
- let start = -1;
137
- for (let i = 0; i < text.length; i++) {
138
- if (text[i] === "{") {
139
- if (depth === 0) start = i;
140
- depth++;
141
- } else if (text[i] === "}") {
142
- depth--;
143
- if (depth === 0 && start !== -1) {
144
- results.push(text.substring(start, i + 1));
145
- start = -1;
111
+ });
112
+ child.on("close", (code) => {
113
+ entry.exitCode = code;
114
+ entry.status = code === 0 ? "completed" : "error";
115
+ entry.endTime = Date.now();
116
+ entry.process = null;
117
+ });
118
+ child.on("error", (error) => {
119
+ entry.stderr += `
120
+ Process error: ${error.message}`;
121
+ entry.status = "error";
122
+ entry.endTime = Date.now();
123
+ entry.process = null;
124
+ });
125
+ if (timeout > 0) {
126
+ setTimeout(() => {
127
+ if (entry.status === "running") {
128
+ child.kill("SIGTERM");
129
+ setTimeout(() => {
130
+ if (entry.status === "running") {
131
+ child.kill("SIGKILL");
146
132
  }
147
- }
133
+ }, 2e3);
134
+ entry.status = "timeout";
135
+ entry.stderr += `
136
+ Command timed out after ${timeout} seconds`;
148
137
  }
149
- return results;
150
- }
151
- /**
152
- * Valida se um tool call normalizado é válido
153
- */
154
- static isValidToolCall(call) {
155
- return !!(call.id && call.type === "function" && call.function?.name && typeof call.function.arguments === "string");
156
- }
138
+ }, timeout * 1e3);
139
+ }
140
+ runningCommands.set(commandId, entry);
141
+ cleanupOldCommands();
142
+ return {
143
+ success: true,
144
+ command_id: commandId,
145
+ command,
146
+ message: `Command started in background. Use command_status with id "${commandId}" to check progress.`
147
+ };
148
+ } catch (error) {
149
+ return {
150
+ success: false,
151
+ error: `Unexpected error: ${error.message}`
157
152
  };
158
- }
159
- });
160
-
161
- // src/main.ts
162
- import React10 from "react";
163
- import { render } from "ink";
164
- import { EventEmitter as EventEmitter2 } from "events";
165
- import { v4 as uuidv43 } from "uuid";
166
-
167
- // src/app/ui/App.tsx
168
- import { useState as useState6, useEffect as useEffect5, useRef as useRef4, useCallback as useCallback2, memo as memo10 } from "react";
169
- import { Box as Box16, Text as Text15, Static } from "ink";
170
-
171
- // src/app/ui/layout.tsx
172
- import { Box, Text } from "ink";
173
- import { memo } from "react";
174
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
175
- var HeaderComponent = ({
176
- sessionId: sessionId2,
177
- workdir
178
- }) => {
179
- const dirName = workdir.split("/").pop() || workdir;
180
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
181
- /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { color: "magenta", bold: true, children: "bluma \u2014 coding agent" }) }),
182
- /* @__PURE__ */ jsxs(Box, { children: [
183
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "cwd " }),
184
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: dirName }),
185
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " | " }),
186
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "session " }),
187
- /* @__PURE__ */ jsx(Text, { color: "gray", children: sessionId2.slice(0, 8) })
188
- ] })
189
- ] });
190
- };
191
- var Header = memo(HeaderComponent);
192
- var SessionInfoComponent = ({
193
- sessionId: sessionId2,
194
- workdir,
195
- toolsCount,
196
- mcpStatus
197
- }) => /* @__PURE__ */ jsxs(Box, { paddingX: 1, children: [
198
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "mcp " }),
199
- /* @__PURE__ */ jsx(Text, { color: mcpStatus === "connected" ? "green" : "yellow", children: mcpStatus === "connected" ? "+" : "-" }),
200
- toolsCount !== null && /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
201
- " ",
202
- toolsCount,
203
- " tools"
204
- ] }) })
205
- ] });
206
- var SessionInfo = memo(SessionInfoComponent);
207
- var TaskStatusBarComponent = ({
208
- taskName,
209
- mode,
210
- status
211
- }) => {
212
- const modeColors = {
213
- PLANNING: "blue",
214
- EXECUTION: "green",
215
- VERIFICATION: "yellow"
216
- };
217
- return /* @__PURE__ */ jsxs(Box, { paddingX: 1, children: [
218
- /* @__PURE__ */ jsxs(Text, { color: modeColors[mode], bold: true, children: [
219
- "[",
220
- mode.charAt(0),
221
- "]"
222
- ] }),
223
- /* @__PURE__ */ jsxs(Text, { children: [
224
- " ",
225
- taskName
226
- ] }),
227
- status && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
228
- " - ",
229
- status
230
- ] })
231
- ] });
232
- };
233
- var TaskStatusBar = memo(TaskStatusBarComponent);
234
-
235
- // src/app/ui/components/InputPrompt.tsx
236
- import { Box as Box2, Text as Text2, useStdout, useInput as useInput2 } from "ink";
237
-
238
- // src/app/ui/utils/useSimpleInputBuffer.ts
239
- import { useReducer, useRef, useCallback, useEffect } from "react";
240
- import { useInput } from "ink";
241
- function getLineStart(text, cursorPos) {
242
- let pos = cursorPos - 1;
243
- while (pos >= 0 && text[pos] !== "\n") {
244
- pos--;
245
- }
246
- return pos + 1;
247
- }
248
- function getLineEnd(text, cursorPos) {
249
- let pos = cursorPos;
250
- while (pos < text.length && text[pos] !== "\n") {
251
- pos++;
252
- }
253
- return pos;
254
- }
255
- function moveToAdjacentLine(text, cursorPos, direction) {
256
- const currentLineStart = getLineStart(text, cursorPos);
257
- const currentLineEnd = getLineEnd(text, cursorPos);
258
- const column = cursorPos - currentLineStart;
259
- if (direction === "up") {
260
- if (currentLineStart === 0) return cursorPos;
261
- const prevLineEnd = currentLineStart - 1;
262
- const prevLineStart = getLineStart(text, prevLineEnd);
263
- const prevLineLength = prevLineEnd - prevLineStart;
264
- return prevLineStart + Math.min(column, prevLineLength);
265
- } else {
266
- if (currentLineEnd === text.length) return cursorPos;
267
- const nextLineStart = currentLineEnd + 1;
268
- const nextLineEnd = getLineEnd(text, nextLineStart);
269
- const nextLineLength = nextLineEnd - nextLineStart;
270
- return nextLineStart + Math.min(column, nextLineLength);
271
153
  }
272
154
  }
273
- function inputReducer(state, action, viewWidth) {
274
- const adjustView = (newCursorPos, currentViewStart) => {
275
- if (newCursorPos < currentViewStart) {
276
- return newCursorPos;
277
- }
278
- if (newCursorPos >= currentViewStart + viewWidth) {
279
- return newCursorPos - viewWidth + 1;
155
+ async function commandStatus(args) {
156
+ try {
157
+ const {
158
+ command_id,
159
+ wait_seconds = 0,
160
+ output_limit = 1e4
161
+ } = args;
162
+ if (!command_id) {
163
+ return {
164
+ success: false,
165
+ command_id: "",
166
+ status: "error",
167
+ error: "command_id is required"
168
+ };
280
169
  }
281
- return currentViewStart;
282
- };
283
- switch (action.type) {
284
- case "INPUT": {
285
- const cleanInput = action.payload.replace(/(\r\n|\r)/gm, "\n");
286
- const newText = state.text.slice(0, state.cursorPosition) + cleanInput + state.text.slice(state.cursorPosition);
287
- const newCursorPosition = state.cursorPosition + cleanInput.length;
288
- const newViewStart = adjustView(newCursorPosition, state.viewStart);
289
- if (newText === state.text && newCursorPosition === state.cursorPosition) {
290
- return state;
291
- }
292
- return { text: newText, cursorPosition: newCursorPosition, viewStart: newViewStart };
170
+ const entry = runningCommands.get(command_id);
171
+ if (!entry) {
172
+ return {
173
+ success: false,
174
+ command_id,
175
+ status: "not_found",
176
+ error: `Command with id "${command_id}" not found. It may have expired or never existed.`
177
+ };
293
178
  }
294
- case "NEWLINE": {
295
- const newText = state.text.slice(0, state.cursorPosition) + "\n" + state.text.slice(state.cursorPosition);
296
- const newCursorPosition = state.cursorPosition + 1;
297
- const newViewStart = adjustView(newCursorPosition, state.viewStart);
298
- return { text: newText, cursorPosition: newCursorPosition, viewStart: newViewStart };
179
+ if (wait_seconds > 0 && entry.status === "running") {
180
+ await new Promise((resolve) => {
181
+ const checkInterval = setInterval(() => {
182
+ if (entry.status !== "running") {
183
+ clearInterval(checkInterval);
184
+ resolve();
185
+ }
186
+ }, 100);
187
+ setTimeout(() => {
188
+ clearInterval(checkInterval);
189
+ resolve();
190
+ }, wait_seconds * 1e3);
191
+ });
299
192
  }
300
- case "MOVE_CURSOR": {
301
- let newCursorPosition = state.cursorPosition;
302
- if (action.direction === "left" && state.cursorPosition > 0) {
303
- newCursorPosition--;
304
- } else if (action.direction === "right" && state.cursorPosition < state.text.length) {
305
- newCursorPosition++;
306
- } else if (action.direction === "up") {
307
- newCursorPosition = moveToAdjacentLine(state.text, state.cursorPosition, "up");
308
- } else if (action.direction === "down") {
309
- newCursorPosition = moveToAdjacentLine(state.text, state.cursorPosition, "down");
310
- }
311
- if (newCursorPosition === state.cursorPosition) {
312
- return state;
313
- }
314
- const newViewStart = adjustView(newCursorPosition, state.viewStart);
315
- return { ...state, cursorPosition: newCursorPosition, viewStart: newViewStart };
193
+ let stdout = entry.stdout;
194
+ let stderr = entry.stderr;
195
+ let truncated = false;
196
+ if (stdout.length > output_limit) {
197
+ stdout = "..." + stdout.substring(stdout.length - output_limit);
198
+ truncated = true;
316
199
  }
317
- case "MOVE_LINE_START": {
318
- const newCursorPosition = getLineStart(state.text, state.cursorPosition);
319
- if (newCursorPosition === state.cursorPosition) {
320
- return state;
321
- }
322
- const newViewStart = adjustView(newCursorPosition, state.viewStart);
323
- return { ...state, cursorPosition: newCursorPosition, viewStart: newViewStart };
200
+ if (stderr.length > output_limit) {
201
+ stderr = "..." + stderr.substring(stderr.length - output_limit);
202
+ truncated = true;
324
203
  }
325
- case "MOVE_LINE_END": {
326
- const newCursorPosition = getLineEnd(state.text, state.cursorPosition);
327
- if (newCursorPosition === state.cursorPosition) {
328
- return state;
329
- }
330
- const newViewStart = adjustView(newCursorPosition, state.viewStart);
331
- return { ...state, cursorPosition: newCursorPosition, viewStart: newViewStart };
204
+ const duration = entry.endTime ? (entry.endTime - entry.startTime) / 1e3 : (Date.now() - entry.startTime) / 1e3;
205
+ return {
206
+ success: true,
207
+ command_id,
208
+ status: entry.status,
209
+ stdout: stdout || void 0,
210
+ stderr: stderr || void 0,
211
+ exit_code: entry.exitCode,
212
+ duration_seconds: Math.round(duration * 10) / 10,
213
+ truncated
214
+ };
215
+ } catch (error) {
216
+ return {
217
+ success: false,
218
+ command_id: args.command_id || "",
219
+ status: "error",
220
+ error: `Unexpected error: ${error.message}`
221
+ };
222
+ }
223
+ }
224
+ async function sendCommandInput(args) {
225
+ try {
226
+ const { command_id, input } = args;
227
+ if (!command_id || input === void 0) {
228
+ return {
229
+ success: false,
230
+ error: "command_id and input are required"
231
+ };
332
232
  }
333
- case "BACKSPACE": {
334
- if (state.cursorPosition > 0) {
335
- const newText = state.text.slice(0, state.cursorPosition - 1) + state.text.slice(state.cursorPosition);
336
- const newCursorPosition = state.cursorPosition - 1;
337
- const newViewStart = adjustView(newCursorPosition, state.viewStart);
338
- return { text: newText, cursorPosition: newCursorPosition, viewStart: newViewStart };
339
- }
340
- return state;
341
- }
342
- case "DELETE": {
343
- if (state.cursorPosition < state.text.length) {
344
- const newText = state.text.slice(0, state.cursorPosition) + state.text.slice(state.cursorPosition + 1);
345
- const newViewStart = adjustView(state.cursorPosition, state.viewStart);
346
- return { text: newText, cursorPosition: state.cursorPosition, viewStart: newViewStart };
347
- }
348
- return state;
349
- }
350
- case "SUBMIT": {
351
- return { text: "", cursorPosition: 0, viewStart: 0 };
352
- }
353
- case "SET": {
354
- const t = action.payload.text.replace(/(\r\n|\r)/gm, "\n");
355
- let newCursorPosition;
356
- if (typeof action.payload.cursorPosition === "number") {
357
- newCursorPosition = Math.min(action.payload.cursorPosition, t.length);
358
- } else if (action.payload.moveCursorToEnd ?? true) {
359
- newCursorPosition = t.length;
360
- } else {
361
- newCursorPosition = Math.min(state.cursorPosition, t.length);
362
- }
363
- const newText = t;
364
- const newViewStart = adjustView(newCursorPosition, 0);
365
- return { text: newText, cursorPosition: newCursorPosition, viewStart: newViewStart };
233
+ const entry = runningCommands.get(command_id);
234
+ if (!entry) {
235
+ return {
236
+ success: false,
237
+ error: `Command with id "${command_id}" not found`
238
+ };
366
239
  }
367
- case "SET_CURSOR": {
368
- const newCursorPosition = Math.max(0, Math.min(action.payload, state.text.length));
369
- if (newCursorPosition === state.cursorPosition) {
370
- return state;
371
- }
372
- const newViewStart = adjustView(newCursorPosition, state.viewStart);
373
- return { ...state, cursorPosition: newCursorPosition, viewStart: newViewStart };
240
+ if (entry.status !== "running" || !entry.process) {
241
+ return {
242
+ success: false,
243
+ error: `Command is not running (status: ${entry.status})`
244
+ };
374
245
  }
375
- default:
376
- return state;
246
+ entry.process.stdin?.write(input);
247
+ return {
248
+ success: true,
249
+ message: `Sent ${input.length} characters to command ${command_id}`
250
+ };
251
+ } catch (error) {
252
+ return {
253
+ success: false,
254
+ error: `Failed to send input: ${error.message}`
255
+ };
377
256
  }
378
257
  }
379
- var useCustomInput = ({ onSubmit, viewWidth, isReadOnly, onInterrupt }) => {
380
- const [state, dispatch] = useReducer(
381
- (s, a) => inputReducer(s, a, viewWidth),
382
- { text: "", cursorPosition: 0, viewStart: 0 }
383
- );
384
- const inputBuffer = useRef("");
385
- const flushScheduled = useRef(false);
386
- const flushInputBuffer = useCallback(() => {
387
- if (inputBuffer.current.length > 0) {
388
- const buffered = inputBuffer.current;
389
- inputBuffer.current = "";
390
- dispatch({ type: "INPUT", payload: buffered });
258
+ async function killCommand(args) {
259
+ try {
260
+ const { command_id } = args;
261
+ const entry = runningCommands.get(command_id);
262
+ if (!entry) {
263
+ return {
264
+ success: false,
265
+ error: `Command with id "${command_id}" not found`
266
+ };
391
267
  }
392
- flushScheduled.current = false;
393
- }, []);
394
- useEffect(() => {
395
- return () => {
396
- if (flushScheduled.current) {
397
- flushInputBuffer();
398
- }
268
+ if (entry.status !== "running" || !entry.process) {
269
+ return {
270
+ success: false,
271
+ error: `Command is not running (status: ${entry.status})`
272
+ };
273
+ }
274
+ entry.process.kill("SIGTERM");
275
+ entry.status = "killed";
276
+ entry.endTime = Date.now();
277
+ return {
278
+ success: true,
279
+ message: `Command ${command_id} killed`
399
280
  };
400
- }, [flushInputBuffer]);
401
- useInput(
402
- (input, key) => {
403
- const hasBackspaceFlag = key.backspace;
404
- const hasDeleteFlag = key.delete;
405
- const hasBackspaceChar = input === "\x7F" || input === "\b" || input === "\b" || input.charCodeAt(0) === 127 || input.charCodeAt(0) === 8;
406
- if (hasBackspaceFlag || hasBackspaceChar) {
407
- if (inputBuffer.current.length > 0) {
408
- flushInputBuffer();
409
- }
410
- dispatch({ type: "BACKSPACE" });
411
- return;
412
- }
413
- if (hasDeleteFlag && (key.ctrl || key.meta)) {
414
- if (inputBuffer.current.length > 0) {
415
- flushInputBuffer();
281
+ } catch (error) {
282
+ return {
283
+ success: false,
284
+ error: `Failed to kill command: ${error.message}`
285
+ };
286
+ }
287
+ }
288
+ var runningCommands, MAX_OUTPUT_SIZE, MAX_STORED_COMMANDS, OUTPUT_TRUNCATION_MSG, DANGEROUS_PATTERNS, INTERACTIVE_PATTERNS;
289
+ var init_async_command = __esm({
290
+ "src/app/agent/tools/natives/async_command.ts"() {
291
+ "use strict";
292
+ runningCommands = /* @__PURE__ */ new Map();
293
+ MAX_OUTPUT_SIZE = 3e4;
294
+ MAX_STORED_COMMANDS = 50;
295
+ OUTPUT_TRUNCATION_MSG = "\n[OUTPUT TRUNCATED - 30KB/200 lines max]";
296
+ DANGEROUS_PATTERNS = [
297
+ // Elevação de privilégios
298
+ /^sudo\s+/i,
299
+ /^doas\s+/i,
300
+ /^su\s+/i,
301
+ /^pkexec\s+/i,
302
+ /\|\s*sudo\s+/i,
303
+ /;\s*sudo\s+/i,
304
+ /&&\s*sudo\s+/i,
305
+ // Comandos destrutivos
306
+ /\brm\s+(-[rf]+\s+)*[\/~]/i,
307
+ /\brm\s+-[rf]*\s+\*/i,
308
+ /\bchmod\s+(777|666)\s+\//i,
309
+ /\bdd\s+.*of=\/dev\/(sd|hd|nvme)/i,
310
+ /\bmkfs\./i,
311
+ // Fork bombs
312
+ /:\(\)\s*\{\s*:\|:&\s*\}\s*;:/,
313
+ // Remote code exec
314
+ /\bcurl\s+.*\|\s*(ba)?sh/i,
315
+ /\bwget\s+.*\|\s*(ba)?sh/i
316
+ ];
317
+ INTERACTIVE_PATTERNS = [
318
+ /^(vim|vi|nano|emacs|pico)\s*/i,
319
+ /^(less|more|most)\s*/i,
320
+ /^(top|htop|btop|atop|nmon)\s*/i,
321
+ /^(ssh|telnet|ftp|sftp)\s+/i,
322
+ /^(mysql|psql|redis-cli|mongo|mongosh)\s*$/i,
323
+ /^(python|python3|node|ruby|irb|lua)\s*$/i,
324
+ /^(gdb|lldb)\s*/i
325
+ ];
326
+ }
327
+ });
328
+
329
+ // src/app/agent/core/llm/tool_call_normalizer.ts
330
+ import { randomUUID } from "crypto";
331
+ var ToolCallNormalizer;
332
+ var init_tool_call_normalizer = __esm({
333
+ "src/app/agent/core/llm/tool_call_normalizer.ts"() {
334
+ "use strict";
335
+ ToolCallNormalizer = class {
336
+ /**
337
+ * Normaliza a mensagem do assistant, convertendo diferentes formatos de tool calls
338
+ */
339
+ static normalizeAssistantMessage(message) {
340
+ if (message.tool_calls && this.isOpenAIFormat(message.tool_calls)) {
341
+ return message;
416
342
  }
417
- dispatch({ type: "DELETE" });
418
- return;
419
- }
420
- if (hasDeleteFlag && !key.ctrl && !key.meta) {
421
- if (inputBuffer.current.length > 0) {
422
- flushInputBuffer();
343
+ const toolCalls = this.extractToolCalls(message);
344
+ if (toolCalls.length > 0) {
345
+ return {
346
+ role: message.role || "assistant",
347
+ content: message.content || null,
348
+ tool_calls: toolCalls
349
+ };
423
350
  }
424
- dispatch({ type: "BACKSPACE" });
425
- return;
426
- }
427
- if (inputBuffer.current.length > 0 && (key.ctrl || key.meta || key.escape || key.return || key.leftArrow || key.rightArrow || key.upArrow || key.downArrow || key.tab || key.shift)) {
428
- flushInputBuffer();
351
+ return message;
429
352
  }
430
- if (key.escape) {
431
- onInterrupt();
432
- return;
353
+ /**
354
+ * Verifica se já está no formato OpenAI
355
+ */
356
+ static isOpenAIFormat(toolCalls) {
357
+ if (!Array.isArray(toolCalls) || toolCalls.length === 0) return false;
358
+ const firstCall = toolCalls[0];
359
+ return typeof firstCall.id === "string" && firstCall.type === "function" && typeof firstCall.function?.name === "string" && typeof firstCall.function?.arguments === "string";
433
360
  }
434
- if (isReadOnly) {
435
- if (key.return && !key.shift) {
436
- if (state.text.trim().length > 0) {
437
- onSubmit(state.text);
438
- dispatch({ type: "SUBMIT" });
361
+ /**
362
+ * Extrai tool calls de diversos formatos possíveis
363
+ */
364
+ static extractToolCalls(message) {
365
+ const results = [];
366
+ if (message.tool_calls && Array.isArray(message.tool_calls)) {
367
+ for (const call of message.tool_calls) {
368
+ const normalized = this.normalizeToolCall(call);
369
+ if (normalized) results.push(normalized);
439
370
  }
440
- return;
441
371
  }
442
- if (key.shift && key.return) {
443
- dispatch({ type: "NEWLINE" });
444
- return;
372
+ if (typeof message.content === "string" && message.content.trim()) {
373
+ const extracted = this.extractFromContent(message.content);
374
+ results.push(...extracted);
445
375
  }
446
- if (key.leftArrow) return dispatch({ type: "MOVE_CURSOR", direction: "left" });
447
- if (key.rightArrow) return dispatch({ type: "MOVE_CURSOR", direction: "right" });
448
- if (key.upArrow) return dispatch({ type: "MOVE_CURSOR", direction: "up" });
449
- if (key.downArrow) return dispatch({ type: "MOVE_CURSOR", direction: "down" });
450
- if (key.ctrl || key.meta || key.tab) return;
451
- inputBuffer.current += input;
452
- if (!flushScheduled.current) {
453
- flushScheduled.current = true;
454
- queueMicrotask(flushInputBuffer);
376
+ if (message.function_call) {
377
+ const normalized = this.normalizeToolCall(message.function_call);
378
+ if (normalized) results.push(normalized);
455
379
  }
456
- return;
457
- }
458
- if (key.return && key.shift) {
459
- dispatch({ type: "NEWLINE" });
460
- return;
380
+ return results;
461
381
  }
462
- if (key.return) {
463
- if (globalThis.__BLUMA_AT_OPEN__) return;
464
- if (globalThis.__BLUMA_SUPPRESS_SUBMIT__) {
465
- globalThis.__BLUMA_SUPPRESS_SUBMIT__ = false;
466
- return;
382
+ /**
383
+ * Normaliza um único tool call para o formato OpenAI
384
+ */
385
+ static normalizeToolCall(call) {
386
+ try {
387
+ if (call.id && call.function?.name) {
388
+ return {
389
+ id: call.id,
390
+ type: "function",
391
+ function: {
392
+ name: call.function.name,
393
+ arguments: typeof call.function.arguments === "string" ? call.function.arguments : JSON.stringify(call.function.arguments)
394
+ }
395
+ };
396
+ }
397
+ if (call.name) {
398
+ return {
399
+ id: call.id || randomUUID(),
400
+ type: "function",
401
+ function: {
402
+ name: call.name,
403
+ arguments: typeof call.arguments === "string" ? call.arguments : JSON.stringify(call.arguments || {})
404
+ }
405
+ };
406
+ }
407
+ if (call.function && typeof call.function === "object") {
408
+ return {
409
+ id: call.id || randomUUID(),
410
+ type: "function",
411
+ function: {
412
+ name: call.function.name,
413
+ arguments: typeof call.function.arguments === "string" ? call.function.arguments : JSON.stringify(call.function.arguments || {})
414
+ }
415
+ };
416
+ }
417
+ return null;
418
+ } catch (error) {
419
+ console.error("Error normalizing tool call:", error, call);
420
+ return null;
467
421
  }
468
- if (state.text.trim().length > 0) {
469
- onSubmit(state.text);
470
- dispatch({ type: "SUBMIT" });
471
- }
472
- return;
473
- }
474
- if (key.leftArrow) return dispatch({ type: "MOVE_CURSOR", direction: "left" });
475
- if (key.rightArrow) return dispatch({ type: "MOVE_CURSOR", direction: "right" });
476
- if (key.upArrow) return dispatch({ type: "MOVE_CURSOR", direction: "up" });
477
- if (key.downArrow) return dispatch({ type: "MOVE_CURSOR", direction: "down" });
478
- if (key.ctrl && input === "a") {
479
- return dispatch({ type: "MOVE_LINE_START" });
480
- }
481
- if (key.ctrl && input === "e") {
482
- return dispatch({ type: "MOVE_LINE_END" });
483
- }
484
- if (key.ctrl || key.meta || key.tab) return;
485
- inputBuffer.current += input;
486
- if (!flushScheduled.current) {
487
- flushScheduled.current = true;
488
- queueMicrotask(flushInputBuffer);
489
422
  }
490
- },
491
- { isActive: true }
492
- );
493
- return {
494
- text: state.text,
495
- cursorPosition: state.cursorPosition,
496
- viewStart: state.viewStart,
497
- setText: useCallback((t, pos) => {
498
- if (inputBuffer.current.length > 0) {
499
- flushInputBuffer();
423
+ /**
424
+ * Extrai tool calls do content (pode estar em markdown, JSON, etc)
425
+ */
426
+ static extractFromContent(content) {
427
+ const results = [];
428
+ const cleanContent = content.replace(/```(?:json)?\s*([\s\S]*?)```/g, "$1");
429
+ const jsonMatches = this.extractJsonObjects(cleanContent);
430
+ for (const jsonStr of jsonMatches) {
431
+ try {
432
+ const parsed = JSON.parse(jsonStr);
433
+ if (Array.isArray(parsed)) {
434
+ for (const call of parsed) {
435
+ const normalized = this.normalizeToolCall(call);
436
+ if (normalized) results.push(normalized);
437
+ }
438
+ } else if (parsed.name || parsed.function) {
439
+ const normalized = this.normalizeToolCall(parsed);
440
+ if (normalized) results.push(normalized);
441
+ } else if (parsed.tool_calls && Array.isArray(parsed.tool_calls)) {
442
+ for (const call of parsed.tool_calls) {
443
+ const normalized = this.normalizeToolCall(call);
444
+ if (normalized) results.push(normalized);
445
+ }
446
+ }
447
+ } catch (e) {
448
+ }
449
+ }
450
+ return results;
500
451
  }
501
- if (typeof pos === "number") {
502
- dispatch({ type: "SET", payload: { text: t, moveCursorToEnd: false, cursorPosition: pos } });
503
- } else {
504
- dispatch({ type: "SET", payload: { text: t, moveCursorToEnd: true } });
452
+ /**
453
+ * Extrai objetos JSON de uma string (suporta múltiplos objetos)
454
+ */
455
+ static extractJsonObjects(text) {
456
+ const results = [];
457
+ let depth = 0;
458
+ let start = -1;
459
+ for (let i = 0; i < text.length; i++) {
460
+ if (text[i] === "{") {
461
+ if (depth === 0) start = i;
462
+ depth++;
463
+ } else if (text[i] === "}") {
464
+ depth--;
465
+ if (depth === 0 && start !== -1) {
466
+ results.push(text.substring(start, i + 1));
467
+ start = -1;
468
+ }
469
+ }
470
+ }
471
+ return results;
505
472
  }
506
- }, [flushInputBuffer]),
507
- setCursor: useCallback((pos) => {
508
- if (inputBuffer.current.length > 0) {
509
- flushInputBuffer();
473
+ /**
474
+ * Valida se um tool call normalizado é válido
475
+ */
476
+ static isValidToolCall(call) {
477
+ return !!(call.id && call.type === "function" && call.function?.name && typeof call.function.arguments === "string");
510
478
  }
511
- dispatch({ type: "SET_CURSOR", payload: pos });
512
- }, [flushInputBuffer])
479
+ };
480
+ }
481
+ });
482
+
483
+ // src/main.ts
484
+ import React10 from "react";
485
+ import { render } from "ink";
486
+ import { EventEmitter as EventEmitter2 } from "events";
487
+ import { v4 as uuidv43 } from "uuid";
488
+
489
+ // src/app/ui/App.tsx
490
+ import { useState as useState6, useEffect as useEffect5, useRef as useRef4, useCallback as useCallback2, memo as memo10 } from "react";
491
+ import { Box as Box16, Text as Text15, Static } from "ink";
492
+
493
+ // src/app/ui/layout.tsx
494
+ import { Box, Text } from "ink";
495
+ import { memo } from "react";
496
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
497
+ var HeaderComponent = ({
498
+ sessionId: sessionId2,
499
+ workdir
500
+ }) => {
501
+ const dirName = workdir.split("/").pop() || workdir;
502
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
503
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { color: "magenta", bold: true, children: "bluma \u2014 coding agent" }) }),
504
+ /* @__PURE__ */ jsxs(Box, { children: [
505
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "cwd " }),
506
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: dirName }),
507
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " | " }),
508
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "session " }),
509
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: sessionId2.slice(0, 8) })
510
+ ] })
511
+ ] });
512
+ };
513
+ var Header = memo(HeaderComponent);
514
+ var SessionInfoComponent = ({
515
+ sessionId: sessionId2,
516
+ workdir,
517
+ toolsCount,
518
+ mcpStatus
519
+ }) => /* @__PURE__ */ jsxs(Box, { paddingX: 1, children: [
520
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "mcp " }),
521
+ /* @__PURE__ */ jsx(Text, { color: mcpStatus === "connected" ? "green" : "yellow", children: mcpStatus === "connected" ? "+" : "-" }),
522
+ toolsCount !== null && /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
523
+ " ",
524
+ toolsCount,
525
+ " tools"
526
+ ] }) })
527
+ ] });
528
+ var SessionInfo = memo(SessionInfoComponent);
529
+ var TaskStatusBarComponent = ({
530
+ taskName,
531
+ mode,
532
+ status
533
+ }) => {
534
+ const modeColors = {
535
+ PLANNING: "blue",
536
+ EXECUTION: "green",
537
+ VERIFICATION: "yellow"
513
538
  };
539
+ return /* @__PURE__ */ jsxs(Box, { paddingX: 1, children: [
540
+ /* @__PURE__ */ jsxs(Text, { color: modeColors[mode], bold: true, children: [
541
+ "[",
542
+ mode.charAt(0),
543
+ "]"
544
+ ] }),
545
+ /* @__PURE__ */ jsxs(Text, { children: [
546
+ " ",
547
+ taskName
548
+ ] }),
549
+ status && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
550
+ " - ",
551
+ status
552
+ ] })
553
+ ] });
514
554
  };
555
+ var TaskStatusBar = memo(TaskStatusBarComponent);
515
556
 
516
557
  // src/app/ui/components/InputPrompt.tsx
517
- import { useEffect as useEffect3, useMemo, useState as useState2, memo as memo2 } from "react";
518
- import { EventEmitter } from "events";
519
-
520
- // src/app/ui/utils/slashRegistry.ts
521
- var getSlashCommands = () => [
522
- { name: "/help", description: "list commands" },
523
- { name: "/mcp", description: "list tools connected via MCP" },
524
- { name: "/tools", description: "list native tools" },
525
- { name: "/init", description: "create a new BluMa.md file with codebase documentation" },
526
- { name: "/clear", description: "clear history" }
527
- ];
528
- var filterSlashCommands = (query) => {
529
- const list = getSlashCommands();
530
- const q = (query || "").toLowerCase();
531
- if (!q) return list;
532
- const scored = list.map((c) => {
533
- const name = c.name.toLowerCase();
534
- const desc = c.description.toLowerCase();
535
- const isPrefix = name.startsWith(q);
536
- const nameIdx = name.indexOf(q);
537
- const descIdx = desc.indexOf(q);
538
- let tier = 3;
539
- let scorePrimary = 0;
540
- let scoreSecondary = Number.MAX_SAFE_INTEGER;
541
- let scoreTertiary = name.length;
542
- if (isPrefix) {
543
- tier = 0;
544
- scorePrimary = q.length * -1;
545
- scoreSecondary = 0;
546
- } else if (nameIdx >= 0) {
547
- tier = 1;
548
- scorePrimary = 0;
549
- scoreSecondary = nameIdx;
550
- } else if (descIdx >= 0) {
551
- tier = 2;
552
- scorePrimary = 0;
553
- scoreSecondary = descIdx;
554
- }
555
- return { cmd: c, tier, scorePrimary, scoreSecondary, scoreTertiary };
556
- }).filter((s) => s.tier !== 3).sort((a, b) => {
557
- if (a.tier !== b.tier) return a.tier - b.tier;
558
- if (a.scorePrimary !== b.scorePrimary) return a.scorePrimary - b.scorePrimary;
559
- if (a.scoreSecondary !== b.scoreSecondary) return a.scoreSecondary - b.scoreSecondary;
560
- if (a.scoreTertiary !== b.scoreTertiary) return a.scoreTertiary - b.scoreTertiary;
561
- return a.cmd.name.localeCompare(b.cmd.name);
562
- });
563
- return scored.map((s) => s.cmd);
564
- };
558
+ import { Box as Box2, Text as Text2, useStdout, useInput as useInput2 } from "ink";
565
559
 
566
- // src/app/ui/hooks/useAtCompletion.ts
567
- import { useEffect as useEffect2, useRef as useRef2, useState } from "react";
568
- import fs from "fs";
569
- import path from "path";
570
- var MAX_RESULTS = 50;
571
- var DEFAULT_RECURSIVE_DEPTH = 2;
572
- function listPathSuggestions(baseDir, pattern) {
573
- const raw = pattern || "";
574
- const patternEndsWithSlash = raw.endsWith("/");
575
- const relDir = raw.replace(/^\/+|\/+$/g, "");
576
- const filterPrefix = patternEndsWithSlash ? "" : relDir.split("/").slice(-1)[0] || "";
577
- const listDir = path.resolve(baseDir, relDir || ".");
578
- const results = [];
579
- const IGNORED_DIRS = ["node_modules", ".git", ".venv", "dist", "build"];
580
- const IGNORED_EXTS = [".pyc", ".class", ".o", ".map", ".log", ".tmp"];
581
- function isIgnoredName(name) {
582
- if (!name) return false;
583
- if (IGNORED_DIRS.includes(name)) return true;
584
- if (name.startsWith(".")) return true;
585
- return false;
560
+ // src/app/ui/utils/useSimpleInputBuffer.ts
561
+ import { useReducer, useRef, useCallback, useEffect } from "react";
562
+ import { useInput } from "ink";
563
+ function getLineStart(text, cursorPos) {
564
+ let pos = cursorPos - 1;
565
+ while (pos >= 0 && text[pos] !== "\n") {
566
+ pos--;
586
567
  }
587
- function isIgnoredFile(name) {
588
- if (!name) return false;
589
- for (const e of IGNORED_EXTS) if (name.endsWith(e)) return true;
590
- return false;
568
+ return pos + 1;
569
+ }
570
+ function getLineEnd(text, cursorPos) {
571
+ let pos = cursorPos;
572
+ while (pos < text.length && text[pos] !== "\n") {
573
+ pos++;
591
574
  }
592
- function pushEntry(entryPath, label, isDir) {
593
- if (results.length >= MAX_RESULTS) return;
594
- const clean = label.split(path.sep).join("/").replace(/[]+/g, "");
595
- results.push({ label: clean + (isDir ? "/" : ""), fullPath: entryPath, isDir });
575
+ return pos;
576
+ }
577
+ function moveToAdjacentLine(text, cursorPos, direction) {
578
+ const currentLineStart = getLineStart(text, cursorPos);
579
+ const currentLineEnd = getLineEnd(text, cursorPos);
580
+ const column = cursorPos - currentLineStart;
581
+ if (direction === "up") {
582
+ if (currentLineStart === 0) return cursorPos;
583
+ const prevLineEnd = currentLineStart - 1;
584
+ const prevLineStart = getLineStart(text, prevLineEnd);
585
+ const prevLineLength = prevLineEnd - prevLineStart;
586
+ return prevLineStart + Math.min(column, prevLineLength);
587
+ } else {
588
+ if (currentLineEnd === text.length) return cursorPos;
589
+ const nextLineStart = currentLineEnd + 1;
590
+ const nextLineEnd = getLineEnd(text, nextLineStart);
591
+ const nextLineLength = nextLineEnd - nextLineStart;
592
+ return nextLineStart + Math.min(column, nextLineLength);
596
593
  }
597
- try {
598
- if (raw.length === 0 || patternEndsWithSlash) {
599
- const queue = [{ dir: listDir, depth: 0, rel: relDir }];
600
- while (queue.length && results.length < MAX_RESULTS) {
601
- const node = queue.shift();
602
- try {
603
- const entries = fs.readdirSync(node.dir, { withFileTypes: true });
604
- for (const entry of entries) {
605
- if (isIgnoredName(entry.name)) continue;
606
- const entryAbs = path.join(node.dir, entry.name);
607
- const entryRel = node.rel ? path.posix.join(node.rel, entry.name) : entry.name;
608
- if (entryRel.split("/").includes("node_modules")) continue;
609
- if (!entry.isDirectory() && isIgnoredFile(entry.name)) continue;
610
- pushEntry(entryAbs, entryRel, entry.isDirectory());
611
- if (entry.isDirectory() && node.depth < DEFAULT_RECURSIVE_DEPTH) {
612
- queue.push({ dir: entryAbs, depth: node.depth + 1, rel: node.rel ? node.rel + "/" + entry.name : entry.name + "/" });
613
- }
614
- if (results.length >= MAX_RESULTS) break;
615
- }
616
- } catch (e) {
617
- }
594
+ }
595
+ function inputReducer(state, action, viewWidth) {
596
+ const adjustView = (newCursorPos, currentViewStart) => {
597
+ if (newCursorPos < currentViewStart) {
598
+ return newCursorPos;
599
+ }
600
+ if (newCursorPos >= currentViewStart + viewWidth) {
601
+ return newCursorPos - viewWidth + 1;
602
+ }
603
+ return currentViewStart;
604
+ };
605
+ switch (action.type) {
606
+ case "INPUT": {
607
+ const cleanInput = action.payload.replace(/(\r\n|\r)/gm, "\n");
608
+ const newText = state.text.slice(0, state.cursorPosition) + cleanInput + state.text.slice(state.cursorPosition);
609
+ const newCursorPosition = state.cursorPosition + cleanInput.length;
610
+ const newViewStart = adjustView(newCursorPosition, state.viewStart);
611
+ if (newText === state.text && newCursorPosition === state.cursorPosition) {
612
+ return state;
618
613
  }
619
- } else {
620
- const entries = fs.readdirSync(listDir, { withFileTypes: true });
621
- for (const entry of entries) {
622
- if (filterPrefix && !entry.name.startsWith(filterPrefix)) continue;
623
- if (isIgnoredName(entry.name)) continue;
624
- if (!entry.isDirectory() && isIgnoredFile(entry.name)) continue;
625
- const entryAbs = path.join(listDir, entry.name);
626
- const label = relDir ? path.posix.join(relDir, entry.name) : entry.name;
627
- pushEntry(entryAbs, label, entry.isDirectory());
628
- if (results.length >= MAX_RESULTS) break;
614
+ return { text: newText, cursorPosition: newCursorPosition, viewStart: newViewStart };
615
+ }
616
+ case "NEWLINE": {
617
+ const newText = state.text.slice(0, state.cursorPosition) + "\n" + state.text.slice(state.cursorPosition);
618
+ const newCursorPosition = state.cursorPosition + 1;
619
+ const newViewStart = adjustView(newCursorPosition, state.viewStart);
620
+ return { text: newText, cursorPosition: newCursorPosition, viewStart: newViewStart };
621
+ }
622
+ case "MOVE_CURSOR": {
623
+ let newCursorPosition = state.cursorPosition;
624
+ if (action.direction === "left" && state.cursorPosition > 0) {
625
+ newCursorPosition--;
626
+ } else if (action.direction === "right" && state.cursorPosition < state.text.length) {
627
+ newCursorPosition++;
628
+ } else if (action.direction === "up") {
629
+ newCursorPosition = moveToAdjacentLine(state.text, state.cursorPosition, "up");
630
+ } else if (action.direction === "down") {
631
+ newCursorPosition = moveToAdjacentLine(state.text, state.cursorPosition, "down");
629
632
  }
633
+ if (newCursorPosition === state.cursorPosition) {
634
+ return state;
635
+ }
636
+ const newViewStart = adjustView(newCursorPosition, state.viewStart);
637
+ return { ...state, cursorPosition: newCursorPosition, viewStart: newViewStart };
630
638
  }
631
- } catch (e) {
632
- }
633
- return results.slice(0, MAX_RESULTS);
634
- }
635
- function useAtCompletion({
636
- cwd,
637
- text,
638
- cursorPosition,
639
- setText
640
- }) {
641
- const [open, setOpen] = useState(false);
642
- const [selected, setSelected] = useState(0);
643
- const [suggestions, setSuggestions] = useState([]);
644
- const lastQuery = useRef2("");
645
- function scanForAt(text2, pos) {
646
- const before = text2.slice(0, pos);
647
- const m = before.match(/@([\w\/.\-_]*)$/);
648
- if (!m) return { pattern: null, insertStart: -1 };
649
- return { pattern: m[1] || "", insertStart: m.index + 1 };
650
- }
651
- function update(newText, newCursor) {
652
- const res = scanForAt(newText, newCursor);
653
- if (res.pattern !== null && res.pattern.length >= 0) {
654
- setOpen(true);
655
- globalThis.__BLUMA_AT_OPEN__ = true;
656
- const suggs = listPathSuggestions(cwd, res.pattern);
657
- setSuggestions(suggs);
658
- setSelected(0);
659
- lastQuery.current = res.pattern;
660
- } else {
661
- setOpen(false);
662
- globalThis.__BLUMA_AT_OPEN__ = false;
663
- setSuggestions([]);
639
+ case "MOVE_LINE_START": {
640
+ const newCursorPosition = getLineStart(state.text, state.cursorPosition);
641
+ if (newCursorPosition === state.cursorPosition) {
642
+ return state;
643
+ }
644
+ const newViewStart = adjustView(newCursorPosition, state.viewStart);
645
+ return { ...state, cursorPosition: newCursorPosition, viewStart: newViewStart };
664
646
  }
665
- }
666
- useEffect2(() => {
667
- update(text, cursorPosition);
668
- }, [text, cursorPosition, cwd]);
669
- function insertAtSelection() {
670
- if (!open || !suggestions[selected]) return;
671
- const res = scanForAt(text, cursorPosition);
672
- if (!res || res.insertStart < 0) return;
673
- let chosen = suggestions[selected].label;
674
- const isDir = suggestions[selected].isDir;
675
- chosen = chosen.replace(/\\/g, "/").replace(/\|/g, "");
676
- let insertVal = chosen.replace(/\/+$/g, "");
677
- const currentPattern = res.pattern || "";
678
- if (currentPattern.length > 0) {
679
- const normalizedPattern = currentPattern.replace(/\/+$/g, "");
680
- if (insertVal.startsWith(normalizedPattern)) {
681
- insertVal = insertVal.slice(normalizedPattern.length);
682
- insertVal = insertVal.replace(/^\/+/, "");
647
+ case "MOVE_LINE_END": {
648
+ const newCursorPosition = getLineEnd(state.text, state.cursorPosition);
649
+ if (newCursorPosition === state.cursorPosition) {
650
+ return state;
683
651
  }
652
+ const newViewStart = adjustView(newCursorPosition, state.viewStart);
653
+ return { ...state, cursorPosition: newCursorPosition, viewStart: newViewStart };
684
654
  }
685
- insertVal = insertVal.split("\\").join("/");
686
- if (isDir && !insertVal.endsWith("/")) insertVal = insertVal + "/";
687
- const pattern = res.pattern || "";
688
- const lastSlash = pattern.lastIndexOf("/");
689
- const segmentOffset = lastSlash >= 0 ? lastSlash + 1 : 0;
690
- const segmentStart = res.insertStart + segmentOffset;
691
- const before = text.slice(0, segmentStart);
692
- const after = text.slice(cursorPosition);
693
- const newText = before + insertVal + after;
694
- const finalText = newText + " ";
695
- setText(finalText, finalText.length);
696
- globalThis.__BLUMA_FORCE_CURSOR_END__ = true;
697
- update(finalText, finalText.length);
698
- if (isDir) {
699
- setOpen(false);
700
- setSuggestions([]);
701
- setTimeout(() => {
702
- setOpen(true);
703
- update(finalText, finalText.length);
704
- }, 0);
705
- } else {
706
- setOpen(false);
707
- setSuggestions([]);
655
+ case "BACKSPACE": {
656
+ if (state.cursorPosition > 0) {
657
+ const newText = state.text.slice(0, state.cursorPosition - 1) + state.text.slice(state.cursorPosition);
658
+ const newCursorPosition = state.cursorPosition - 1;
659
+ const newViewStart = adjustView(newCursorPosition, state.viewStart);
660
+ return { text: newText, cursorPosition: newCursorPosition, viewStart: newViewStart };
661
+ }
662
+ return state;
708
663
  }
664
+ case "DELETE": {
665
+ if (state.cursorPosition < state.text.length) {
666
+ const newText = state.text.slice(0, state.cursorPosition) + state.text.slice(state.cursorPosition + 1);
667
+ const newViewStart = adjustView(state.cursorPosition, state.viewStart);
668
+ return { text: newText, cursorPosition: state.cursorPosition, viewStart: newViewStart };
669
+ }
670
+ return state;
671
+ }
672
+ case "SUBMIT": {
673
+ return { text: "", cursorPosition: 0, viewStart: 0 };
674
+ }
675
+ case "SET": {
676
+ const t = action.payload.text.replace(/(\r\n|\r)/gm, "\n");
677
+ let newCursorPosition;
678
+ if (typeof action.payload.cursorPosition === "number") {
679
+ newCursorPosition = Math.min(action.payload.cursorPosition, t.length);
680
+ } else if (action.payload.moveCursorToEnd ?? true) {
681
+ newCursorPosition = t.length;
682
+ } else {
683
+ newCursorPosition = Math.min(state.cursorPosition, t.length);
684
+ }
685
+ const newText = t;
686
+ const newViewStart = adjustView(newCursorPosition, 0);
687
+ return { text: newText, cursorPosition: newCursorPosition, viewStart: newViewStart };
688
+ }
689
+ case "SET_CURSOR": {
690
+ const newCursorPosition = Math.max(0, Math.min(action.payload, state.text.length));
691
+ if (newCursorPosition === state.cursorPosition) {
692
+ return state;
693
+ }
694
+ const newViewStart = adjustView(newCursorPosition, state.viewStart);
695
+ return { ...state, cursorPosition: newCursorPosition, viewStart: newViewStart };
696
+ }
697
+ default:
698
+ return state;
709
699
  }
710
- function close() {
711
- setOpen(false);
712
- setSuggestions([]);
713
- }
714
- return { open, suggestions, selected, setSelected, insertAtSelection, close, update };
715
700
  }
716
-
717
- // src/app/ui/components/InputPrompt.tsx
718
- import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
719
- var uiEventBus = global.__bluma_ui_eventbus__ || new EventEmitter();
720
- global.__bluma_ui_eventbus__ = uiEventBus;
721
- var TextLine = memo2(({
722
- line,
723
- lineIndex,
724
- cursorLine,
725
- cursorCol,
726
- showCursor
727
- }) => {
728
- const isCursorLine = lineIndex === cursorLine;
729
- if (!isCursorLine || !showCursor) {
730
- return /* @__PURE__ */ jsx2(Text2, { children: line });
731
- }
732
- const before = line.slice(0, cursorCol);
733
- const char = line[cursorCol] || " ";
734
- const after = line.slice(cursorCol + 1);
735
- return /* @__PURE__ */ jsxs2(Text2, { children: [
736
- before,
737
- /* @__PURE__ */ jsx2(Text2, { inverse: true, color: "white", children: char }),
738
- after
739
- ] });
740
- }, (prev, next) => {
741
- if (prev.line !== next.line) return false;
742
- if (prev.showCursor !== next.showCursor) return false;
743
- const prevIsCursorLine = prev.lineIndex === prev.cursorLine;
744
- const nextIsCursorLine = next.lineIndex === next.cursorLine;
745
- if (prevIsCursorLine !== nextIsCursorLine) return false;
746
- if (nextIsCursorLine && prev.cursorCol !== next.cursorCol) return false;
747
- return true;
748
- });
749
- TextLine.displayName = "TextLine";
750
- var PathSuggestions = memo2(({
751
- suggestions,
752
- selected
753
- }) => {
754
- const VISIBLE = 7;
755
- const total = suggestions.length;
756
- const sel = Math.max(0, Math.min(selected, total - 1));
757
- let start = Math.max(0, sel - Math.floor(VISIBLE / 2));
758
- if (start + VISIBLE > total) start = Math.max(0, total - VISIBLE);
759
- const windowItems = suggestions.slice(start, start + VISIBLE);
760
- return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", marginTop: 1, height: Math.min(VISIBLE, total), children: windowItems.map((s, idx) => {
761
- const realIdx = start + idx;
762
- const isSelected = realIdx === selected;
763
- return /* @__PURE__ */ jsxs2(Box2, { paddingLeft: 1, paddingY: 0, children: [
764
- /* @__PURE__ */ jsx2(Text2, { color: isSelected ? "magenta" : "gray", children: isSelected ? "\u276F " : " " }),
765
- /* @__PURE__ */ jsx2(Text2, { color: isSelected ? "magenta" : "white", bold: isSelected, dimColor: !isSelected, children: s.label })
766
- ] }, s.fullPath);
767
- }) });
768
- });
769
- PathSuggestions.displayName = "PathSuggestions";
770
- var SlashSuggestions = memo2(({
771
- suggestions,
772
- selectedIndex
773
- }) => /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", marginTop: 1, children: suggestions.map((s, idx) => {
774
- const isSelected = idx === selectedIndex;
775
- return /* @__PURE__ */ jsxs2(Box2, { paddingLeft: 1, paddingY: 0, children: [
776
- /* @__PURE__ */ jsx2(Text2, { color: isSelected ? "magenta" : "gray", children: isSelected ? "\u276F " : " " }),
777
- /* @__PURE__ */ jsxs2(Text2, { color: isSelected ? "magenta" : "white", bold: isSelected, dimColor: !isSelected, children: [
778
- s.name,
779
- " ",
780
- /* @__PURE__ */ jsxs2(Text2, { color: "gray", children: [
781
- "- ",
782
- s.description
783
- ] })
784
- ] })
785
- ] }, s.name);
786
- }) }));
787
- SlashSuggestions.displayName = "SlashSuggestions";
788
- var Footer = memo2(({ isReadOnly }) => /* @__PURE__ */ jsx2(Box2, { paddingX: 1, justifyContent: "center", children: /* @__PURE__ */ jsx2(Text2, { color: "gray", dimColor: true, children: isReadOnly ? "ctrl+c to exit | Enter to send message | Shift+Enter for new line | esc interrupt" : "ctrl+c to exit | Enter to submit | Shift+Enter for new line | /help commands | esc interrupt" }) }));
789
- Footer.displayName = "Footer";
790
- var TextLinesRenderer = memo2(({
791
- lines,
792
- cursorLine,
793
- cursorCol,
794
- showCursor,
795
- showPlaceholder,
796
- placeholder
797
- }) => {
798
- return /* @__PURE__ */ jsx2(Fragment2, { children: lines.map((line, idx) => {
799
- const isFirstLine = idx === 0;
800
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", paddingX: 1, children: [
801
- isFirstLine && /* @__PURE__ */ jsxs2(Text2, { color: "white", children: [
802
- ">",
803
- " "
804
- ] }),
805
- !isFirstLine && /* @__PURE__ */ jsxs2(Text2, { color: "gray", children: [
806
- "\u2502",
807
- " "
808
- ] }),
809
- showPlaceholder && isFirstLine && line.length === 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: placeholder }) : /* @__PURE__ */ jsx2(
810
- TextLine,
811
- {
812
- line,
813
- lineIndex: idx,
814
- cursorLine,
815
- cursorCol,
816
- showCursor
817
- }
818
- )
819
- ] }, idx);
820
- }) });
821
- });
822
- TextLinesRenderer.displayName = "TextLinesRenderer";
823
- var InputPrompt = memo2(({
824
- onSubmit,
825
- isReadOnly,
826
- onInterrupt,
827
- disableWhileProcessing = false
828
- }) => {
829
- const { stdout } = useStdout();
830
- const [viewWidth] = useState2(() => stdout.columns - 6);
831
- const [slashOpen, setSlashOpen] = useState2(false);
832
- const [slashIndex, setSlashIndex] = useState2(0);
833
- const permissiveOnSubmit = (value) => {
834
- const trimmed = (value || "").trim();
835
- if (isReadOnly) {
836
- if (trimmed.length > 0) {
837
- uiEventBus.emit("user_overlay", { kind: "message", payload: trimmed, ts: Date.now() });
838
- }
839
- return;
701
+ var useCustomInput = ({ onSubmit, viewWidth, isReadOnly, onInterrupt }) => {
702
+ const [state, dispatch] = useReducer(
703
+ (s, a) => inputReducer(s, a, viewWidth),
704
+ { text: "", cursorPosition: 0, viewStart: 0 }
705
+ );
706
+ const inputBuffer = useRef("");
707
+ const flushScheduled = useRef(false);
708
+ const flushInputBuffer = useCallback(() => {
709
+ if (inputBuffer.current.length > 0) {
710
+ const buffered = inputBuffer.current;
711
+ inputBuffer.current = "";
712
+ dispatch({ type: "INPUT", payload: buffered });
840
713
  }
841
- onSubmit(value);
842
- };
843
- const { text, cursorPosition, setText } = useCustomInput({
844
- onSubmit: (value) => {
845
- if (disableWhileProcessing && isReadOnly) return;
846
- if (pathAutocomplete.open) return;
847
- permissiveOnSubmit(value);
848
- },
849
- viewWidth,
850
- isReadOnly,
851
- onInterrupt
852
- });
853
- const linesData = useMemo(() => {
854
- const lines = text.split("\n");
855
- let remainingChars = cursorPosition;
856
- let cursorLine = 0;
857
- let cursorCol = 0;
858
- for (let i = 0; i < lines.length; i++) {
859
- const lineLength = lines[i].length;
860
- if (remainingChars <= lineLength) {
861
- cursorLine = i;
862
- cursorCol = remainingChars;
863
- break;
714
+ flushScheduled.current = false;
715
+ }, []);
716
+ useEffect(() => {
717
+ return () => {
718
+ if (flushScheduled.current) {
719
+ flushInputBuffer();
864
720
  }
865
- remainingChars -= lineLength + 1;
866
- if (i === lines.length - 1) {
867
- cursorLine = i;
868
- cursorCol = lineLength;
721
+ };
722
+ }, [flushInputBuffer]);
723
+ useInput(
724
+ (input, key) => {
725
+ const hasBackspaceFlag = key.backspace;
726
+ const hasDeleteFlag = key.delete;
727
+ const hasBackspaceChar = input === "\x7F" || input === "\b" || input === "\b" || input.charCodeAt(0) === 127 || input.charCodeAt(0) === 8;
728
+ if (hasBackspaceFlag || hasBackspaceChar) {
729
+ if (inputBuffer.current.length > 0) {
730
+ flushInputBuffer();
731
+ }
732
+ dispatch({ type: "BACKSPACE" });
733
+ return;
869
734
  }
870
- }
871
- return { lines, cursorLine, cursorCol, totalLines: lines.length };
872
- }, [text, cursorPosition]);
873
- const displayData = linesData;
874
- const placeholder = isReadOnly ? " Press Esc to cancel | Enter message while agent runs" : "";
875
- const showPlaceholder = text.length === 0 && isReadOnly;
876
- const slashQuery = useMemo(() => text.startsWith("/") ? text : "", [text]);
877
- const slashSuggestions = useMemo(() => {
878
- if (!slashQuery) return [];
879
- return filterSlashCommands(slashQuery);
880
- }, [slashQuery]);
881
- useEffect3(() => {
882
- if (isReadOnly) {
883
- setSlashOpen(false);
884
- return;
885
- }
886
- if (text.startsWith("/")) {
887
- setSlashOpen(true);
888
- setSlashIndex(0);
889
- } else {
890
- setSlashOpen(false);
891
- }
892
- }, [text, isReadOnly]);
893
- useInput2((input, key) => {
894
- if (!slashOpen) return;
895
- if (key.downArrow) {
896
- setSlashIndex((i) => Math.min(i + 1, Math.max(0, slashSuggestions.length - 1)));
897
- } else if (key.upArrow) {
898
- setSlashIndex((i) => Math.max(i - 1, 0));
899
- } else if (key.return) {
900
- const choice = slashSuggestions[slashIndex];
901
- if (choice) {
902
- setSlashOpen(false);
903
- try {
904
- setText(`${choice.name} `);
905
- } catch (e) {
906
- permissiveOnSubmit(`${choice.name} `);
735
+ if (hasDeleteFlag && (key.ctrl || key.meta)) {
736
+ if (inputBuffer.current.length > 0) {
737
+ flushInputBuffer();
907
738
  }
739
+ dispatch({ type: "DELETE" });
740
+ return;
908
741
  }
909
- } else if (key.escape) {
910
- setSlashOpen(false);
911
- }
912
- }, { isActive: slashOpen });
913
- useEffect3(() => {
914
- if (globalThis.__BLUMA_FORCE_CURSOR_END__) {
915
- setText(text, text.length);
916
- delete globalThis.__BLUMA_FORCE_CURSOR_END__;
917
- }
918
- }, [text, setText]);
919
- const cwd = process.cwd();
920
- const pathAutocomplete = useAtCompletion({ cwd, text, cursorPosition, setText });
921
- useInput2((input, key) => {
922
- if (key.backspace || key.delete || key.ctrl || key.meta) return;
923
- if (pathAutocomplete.open) {
924
- if (key.downArrow) {
925
- pathAutocomplete.setSelected((i) => Math.min(i + 1, Math.max(0, pathAutocomplete.suggestions.length - 1)));
742
+ if (hasDeleteFlag && !key.ctrl && !key.meta) {
743
+ if (inputBuffer.current.length > 0) {
744
+ flushInputBuffer();
745
+ }
746
+ dispatch({ type: "BACKSPACE" });
926
747
  return;
927
- } else if (key.upArrow) {
928
- pathAutocomplete.setSelected((i) => Math.max(i - 1, 0));
748
+ }
749
+ if (inputBuffer.current.length > 0 && (key.ctrl || key.meta || key.escape || key.return || key.leftArrow || key.rightArrow || key.upArrow || key.downArrow || key.tab || key.shift)) {
750
+ flushInputBuffer();
751
+ }
752
+ if (key.escape) {
753
+ onInterrupt();
929
754
  return;
930
- } else if (key.return || key.tab) {
931
- const selected = pathAutocomplete.suggestions[pathAutocomplete.selected];
932
- if (selected) {
933
- const before = text.slice(0, cursorPosition);
934
- const m = before.match(/@([\w\/.\-_]*)$/);
935
- if (m) {
936
- globalThis.__BLUMA_SUPPRESS_SUBMIT__ = true;
937
- pathAutocomplete.insertAtSelection();
938
- return;
755
+ }
756
+ if (isReadOnly) {
757
+ if (key.return && !key.shift) {
758
+ if (state.text.trim().length > 0) {
759
+ onSubmit(state.text);
760
+ dispatch({ type: "SUBMIT" });
939
761
  }
762
+ return;
763
+ }
764
+ if (key.shift && key.return) {
765
+ dispatch({ type: "NEWLINE" });
766
+ return;
767
+ }
768
+ if (key.leftArrow) return dispatch({ type: "MOVE_CURSOR", direction: "left" });
769
+ if (key.rightArrow) return dispatch({ type: "MOVE_CURSOR", direction: "right" });
770
+ if (key.upArrow) return dispatch({ type: "MOVE_CURSOR", direction: "up" });
771
+ if (key.downArrow) return dispatch({ type: "MOVE_CURSOR", direction: "down" });
772
+ if (key.ctrl || key.meta || key.tab) return;
773
+ inputBuffer.current += input;
774
+ if (!flushScheduled.current) {
775
+ flushScheduled.current = true;
776
+ queueMicrotask(flushInputBuffer);
940
777
  }
941
778
  return;
942
- } else if (key.escape) {
943
- pathAutocomplete.close();
779
+ }
780
+ if (key.return && key.shift) {
781
+ dispatch({ type: "NEWLINE" });
944
782
  return;
945
783
  }
946
- return;
947
- }
948
- }, { isActive: true });
949
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
950
- disableWhileProcessing ? /* @__PURE__ */ jsx2(Box2, { children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", paddingX: 1, flexWrap: "nowrap", children: [
951
- /* @__PURE__ */ jsxs2(Text2, { color: "white", children: [
952
- ">",
953
- " "
954
- ] }),
955
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "ctrl+c to exit" })
956
- ] }) }) : /* @__PURE__ */ jsxs2(Fragment2, { children: [
957
- /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: /* @__PURE__ */ jsx2(
958
- TextLinesRenderer,
959
- {
960
- lines: displayData.lines,
961
- cursorLine: displayData.cursorLine,
962
- cursorCol: displayData.cursorCol,
963
- showCursor: !isReadOnly,
964
- showPlaceholder,
965
- placeholder
966
- }
967
- ) }),
968
- pathAutocomplete.open && pathAutocomplete.suggestions.length > 0 && /* @__PURE__ */ jsx2(
969
- PathSuggestions,
970
- {
971
- suggestions: pathAutocomplete.suggestions,
972
- selected: pathAutocomplete.selected
784
+ if (key.return) {
785
+ if (globalThis.__BLUMA_AT_OPEN__) return;
786
+ if (globalThis.__BLUMA_SUPPRESS_SUBMIT__) {
787
+ globalThis.__BLUMA_SUPPRESS_SUBMIT__ = false;
788
+ return;
973
789
  }
974
- ),
975
- slashOpen && slashSuggestions.length > 0 && /* @__PURE__ */ jsx2(
976
- SlashSuggestions,
977
- {
978
- suggestions: slashSuggestions,
979
- selectedIndex: slashIndex
790
+ if (state.text.trim().length > 0) {
791
+ onSubmit(state.text);
792
+ dispatch({ type: "SUBMIT" });
980
793
  }
981
- )
982
- ] }),
983
- /* @__PURE__ */ jsx2(Footer, { isReadOnly })
984
- ] });
985
- });
986
- InputPrompt.displayName = "InputPrompt";
987
-
988
- // src/app/ui/ConfirmationPrompt.tsx
989
- import { memo as memo4 } from "react";
990
- import { Box as Box6, Text as Text6 } from "ink";
991
-
992
- // src/app/ui/InteractiveMenu.tsx
993
- import { useState as useState3, memo as memo3 } from "react";
994
- import { Box as Box3, Text as Text3, useInput as useInput3 } from "ink";
995
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
996
- var InteractiveMenuComponent = ({ onDecision }) => {
997
- const options = [
998
- { key: "y", label: "yes", value: "accept", color: "green" },
999
- { key: "n", label: "no", value: "decline", color: "red" },
1000
- { key: "a", label: "always", value: "accept_always", color: "yellow" }
1001
- ];
1002
- const [selected, setSelected] = useState3(0);
1003
- useInput3((input, key) => {
1004
- if (key.upArrow || key.leftArrow) {
1005
- setSelected((i) => i > 0 ? i - 1 : options.length - 1);
1006
- }
1007
- if (key.downArrow || key.rightArrow) {
1008
- setSelected((i) => i < options.length - 1 ? i + 1 : 0);
1009
- }
1010
- if (key.escape) {
1011
- onDecision("decline");
1012
- }
1013
- if (key.return) {
1014
- onDecision(options[selected].value);
1015
- }
1016
- const opt = options.find((o) => o.key === input.toLowerCase());
1017
- if (opt) {
1018
- onDecision(opt.value);
1019
- }
1020
- });
1021
- return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 1, marginTop: 1, children: [
1022
- /* @__PURE__ */ jsxs3(Box3, { children: [
1023
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "approve? " }),
1024
- options.map((opt, idx) => {
1025
- const isSelected = idx === selected;
1026
- return /* @__PURE__ */ jsx3(Box3, { marginRight: 1, children: /* @__PURE__ */ jsxs3(Text3, { color: isSelected ? opt.color : "gray", bold: isSelected, children: [
1027
- "[",
1028
- opt.key,
1029
- "]",
1030
- opt.label
1031
- ] }) }, opt.value);
1032
- })
1033
- ] }),
1034
- /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "arrows to select, enter to confirm, esc to cancel" }) })
1035
- ] });
1036
- };
1037
- var InteractiveMenu = memo3(InteractiveMenuComponent);
1038
-
1039
- // src/app/ui/components/promptRenderers.tsx
1040
- import { Box as Box5, Text as Text5 } from "ink";
1041
- import path2 from "path";
1042
-
1043
- // src/app/ui/components/SimpleDiff.tsx
1044
- import { Box as Box4, Text as Text4 } from "ink";
1045
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1046
- var SimpleDiff = ({ text, maxHeight }) => {
1047
- const allLines = (text || "").split("\n").filter((line) => line !== "");
1048
- const isTruncated = maxHeight > 0 && allLines.length > maxHeight;
1049
- const linesToRender = isTruncated ? allLines.slice(-maxHeight) : allLines;
1050
- const hiddenCount = allLines.length - linesToRender.length;
1051
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", paddingX: 2, children: [
1052
- isTruncated && /* @__PURE__ */ jsx4(Box4, { marginBottom: 0, children: /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1053
- "\u22EF ",
1054
- hiddenCount,
1055
- " lines hidden"
1056
- ] }) }),
1057
- linesToRender.map((line, index) => {
1058
- if (line.startsWith("---") || line.startsWith("+++")) {
1059
- return null;
794
+ return;
1060
795
  }
1061
- let color = "white";
1062
- let prefix = "";
1063
- if (line.startsWith("+")) {
1064
- color = "green";
1065
- prefix = "+ ";
1066
- } else if (line.startsWith("-")) {
1067
- color = "red";
1068
- prefix = "- ";
1069
- } else if (line.startsWith("@@")) {
1070
- color = "cyan";
1071
- prefix = "";
796
+ if (key.leftArrow) return dispatch({ type: "MOVE_CURSOR", direction: "left" });
797
+ if (key.rightArrow) return dispatch({ type: "MOVE_CURSOR", direction: "right" });
798
+ if (key.upArrow) return dispatch({ type: "MOVE_CURSOR", direction: "up" });
799
+ if (key.downArrow) return dispatch({ type: "MOVE_CURSOR", direction: "down" });
800
+ if (key.ctrl && input === "a") {
801
+ return dispatch({ type: "MOVE_LINE_START" });
802
+ }
803
+ if (key.ctrl && input === "e") {
804
+ return dispatch({ type: "MOVE_LINE_END" });
805
+ }
806
+ if (key.ctrl || key.meta || key.tab) return;
807
+ inputBuffer.current += input;
808
+ if (!flushScheduled.current) {
809
+ flushScheduled.current = true;
810
+ queueMicrotask(flushInputBuffer);
811
+ }
812
+ },
813
+ { isActive: true }
814
+ );
815
+ return {
816
+ text: state.text,
817
+ cursorPosition: state.cursorPosition,
818
+ viewStart: state.viewStart,
819
+ setText: useCallback((t, pos) => {
820
+ if (inputBuffer.current.length > 0) {
821
+ flushInputBuffer();
822
+ }
823
+ if (typeof pos === "number") {
824
+ dispatch({ type: "SET", payload: { text: t, moveCursorToEnd: false, cursorPosition: pos } });
1072
825
  } else {
1073
- color = "gray";
1074
- prefix = " ";
826
+ dispatch({ type: "SET", payload: { text: t, moveCursorToEnd: true } });
1075
827
  }
1076
- return /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs4(Text4, { color, children: [
1077
- prefix,
1078
- line.replace(/^[+\-]/, "")
1079
- ] }) }, index);
1080
- })
1081
- ] });
828
+ }, [flushInputBuffer]),
829
+ setCursor: useCallback((pos) => {
830
+ if (inputBuffer.current.length > 0) {
831
+ flushInputBuffer();
832
+ }
833
+ dispatch({ type: "SET_CURSOR", payload: pos });
834
+ }, [flushInputBuffer])
835
+ };
1082
836
  };
1083
837
 
1084
- // src/app/ui/components/promptRenderers.tsx
1085
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1086
- var getBasePath = (filePath) => {
1087
- return path2.basename(filePath);
1088
- };
1089
- var renderShellCommand = ({ toolCall }) => {
1090
- let command = "";
1091
- try {
1092
- const args = typeof toolCall.function.arguments === "string" ? JSON.parse(toolCall.function.arguments) : toolCall.function.arguments;
1093
- command = args.command || "[command not found]";
1094
- } catch (e) {
1095
- command = "Error parsing command";
1096
- }
1097
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
1098
- /* @__PURE__ */ jsxs5(Box5, { children: [
1099
- /* @__PURE__ */ jsx5(Text5, { color: "blue", children: "\u25B8" }),
1100
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " shell" })
1101
- ] }),
1102
- /* @__PURE__ */ jsx5(Box5, { paddingLeft: 2, children: /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: command }) })
1103
- ] });
838
+ // src/app/ui/components/InputPrompt.tsx
839
+ import { useEffect as useEffect3, useMemo, useState as useState2, memo as memo2 } from "react";
840
+ import { EventEmitter } from "events";
841
+
842
+ // src/app/ui/utils/slashRegistry.ts
843
+ var getSlashCommands = () => [
844
+ { name: "/help", description: "list commands" },
845
+ { name: "/mcp", description: "list tools connected via MCP" },
846
+ { name: "/tools", description: "list native tools" },
847
+ { name: "/init", description: "create a new BluMa.md file with codebase documentation" },
848
+ { name: "/clear", description: "clear history" }
849
+ ];
850
+ var filterSlashCommands = (query) => {
851
+ const list = getSlashCommands();
852
+ const q = (query || "").toLowerCase();
853
+ if (!q) return list;
854
+ const scored = list.map((c) => {
855
+ const name = c.name.toLowerCase();
856
+ const desc = c.description.toLowerCase();
857
+ const isPrefix = name.startsWith(q);
858
+ const nameIdx = name.indexOf(q);
859
+ const descIdx = desc.indexOf(q);
860
+ let tier = 3;
861
+ let scorePrimary = 0;
862
+ let scoreSecondary = Number.MAX_SAFE_INTEGER;
863
+ let scoreTertiary = name.length;
864
+ if (isPrefix) {
865
+ tier = 0;
866
+ scorePrimary = q.length * -1;
867
+ scoreSecondary = 0;
868
+ } else if (nameIdx >= 0) {
869
+ tier = 1;
870
+ scorePrimary = 0;
871
+ scoreSecondary = nameIdx;
872
+ } else if (descIdx >= 0) {
873
+ tier = 2;
874
+ scorePrimary = 0;
875
+ scoreSecondary = descIdx;
876
+ }
877
+ return { cmd: c, tier, scorePrimary, scoreSecondary, scoreTertiary };
878
+ }).filter((s) => s.tier !== 3).sort((a, b) => {
879
+ if (a.tier !== b.tier) return a.tier - b.tier;
880
+ if (a.scorePrimary !== b.scorePrimary) return a.scorePrimary - b.scorePrimary;
881
+ if (a.scoreSecondary !== b.scoreSecondary) return a.scoreSecondary - b.scoreSecondary;
882
+ if (a.scoreTertiary !== b.scoreTertiary) return a.scoreTertiary - b.scoreTertiary;
883
+ return a.cmd.name.localeCompare(b.cmd.name);
884
+ });
885
+ return scored.map((s) => s.cmd);
1104
886
  };
1105
- var renderLsTool = ({ toolCall }) => {
1106
- let directoryPath = "[path not specified]";
1107
- try {
1108
- const args = typeof toolCall.function.arguments === "string" ? JSON.parse(toolCall.function.arguments) : toolCall.function.arguments;
1109
- directoryPath = args.directory_path || "[path not specified]";
1110
- } catch (e) {
1111
- directoryPath = "Error parsing arguments";
887
+
888
+ // src/app/ui/hooks/useAtCompletion.ts
889
+ import { useEffect as useEffect2, useRef as useRef2, useState } from "react";
890
+ import fs from "fs";
891
+ import path from "path";
892
+ var MAX_RESULTS = 50;
893
+ var DEFAULT_RECURSIVE_DEPTH = 2;
894
+ function listPathSuggestions(baseDir, pattern) {
895
+ const raw = pattern || "";
896
+ const patternEndsWithSlash = raw.endsWith("/");
897
+ const relDir = raw.replace(/^\/+|\/+$/g, "");
898
+ const filterPrefix = patternEndsWithSlash ? "" : relDir.split("/").slice(-1)[0] || "";
899
+ const listDir = path.resolve(baseDir, relDir || ".");
900
+ const results = [];
901
+ const IGNORED_DIRS = ["node_modules", ".git", ".venv", "dist", "build"];
902
+ const IGNORED_EXTS = [".pyc", ".class", ".o", ".map", ".log", ".tmp"];
903
+ function isIgnoredName(name) {
904
+ if (!name) return false;
905
+ if (IGNORED_DIRS.includes(name)) return true;
906
+ if (name.startsWith(".")) return true;
907
+ return false;
1112
908
  }
1113
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
1114
- /* @__PURE__ */ jsxs5(Box5, { children: [
1115
- /* @__PURE__ */ jsx5(Text5, { color: "blue", children: "\u25B8" }),
1116
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " ls" })
1117
- ] }),
1118
- /* @__PURE__ */ jsx5(Box5, { paddingLeft: 2, children: /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: directoryPath }) })
1119
- ] });
1120
- };
1121
- var renderCountFilesLinesTool = ({ toolCall }) => {
1122
- let filepath = "[path not specified]";
1123
- try {
1124
- const args = typeof toolCall.function.arguments === "string" ? JSON.parse(toolCall.function.arguments) : toolCall.function.arguments;
1125
- filepath = args.filepath || "[path not specified]";
1126
- } catch (e) {
1127
- filepath = "Error parsing arguments";
909
+ function isIgnoredFile(name) {
910
+ if (!name) return false;
911
+ for (const e of IGNORED_EXTS) if (name.endsWith(e)) return true;
912
+ return false;
1128
913
  }
1129
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
1130
- /* @__PURE__ */ jsxs5(Box5, { children: [
1131
- /* @__PURE__ */ jsx5(Text5, { color: "blue", children: "\u25B8" }),
1132
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " count lines" })
1133
- ] }),
1134
- /* @__PURE__ */ jsx5(Box5, { paddingLeft: 2, children: /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: filepath }) })
1135
- ] });
1136
- };
1137
- var renderReadFileLines = ({ toolCall }) => {
1138
- let filepath = "[path not specified]";
1139
- let startLine = 0;
1140
- let endLine = 0;
1141
- try {
1142
- const args = typeof toolCall.function.arguments === "string" ? JSON.parse(toolCall.function.arguments) : toolCall.function.arguments;
1143
- filepath = args.filepath || "[path not specified]";
1144
- startLine = args.start_line || 0;
1145
- endLine = args.end_line || 0;
1146
- } catch (e) {
1147
- filepath = "Error parsing arguments";
914
+ function pushEntry(entryPath, label, isDir) {
915
+ if (results.length >= MAX_RESULTS) return;
916
+ const clean = label.split(path.sep).join("/").replace(/[]+/g, "");
917
+ results.push({ label: clean + (isDir ? "/" : ""), fullPath: entryPath, isDir });
1148
918
  }
1149
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
1150
- /* @__PURE__ */ jsxs5(Box5, { children: [
1151
- /* @__PURE__ */ jsx5(Text5, { color: "blue", children: "\u25B8" }),
1152
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " read" })
1153
- ] }),
1154
- /* @__PURE__ */ jsxs5(Box5, { paddingLeft: 2, flexDirection: "column", children: [
1155
- /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: filepath }),
1156
- /* @__PURE__ */ jsxs5(Box5, { paddingLeft: 2, children: [
1157
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "lines " }),
1158
- /* @__PURE__ */ jsx5(Text5, { color: "magenta", children: startLine }),
1159
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " to " }),
1160
- /* @__PURE__ */ jsx5(Text5, { color: "magenta", children: endLine })
1161
- ] })
1162
- ] })
1163
- ] });
1164
- };
1165
- var renderEditTool = ({ toolCall, preview }) => {
1166
- const diffMaxHeight = 5;
1167
- let filepath = "[path not specified]";
1168
919
  try {
1169
- const args = JSON.parse(toolCall.function.arguments);
1170
- filepath = args.file_path || "[path not specified]";
920
+ if (raw.length === 0 || patternEndsWithSlash) {
921
+ const queue = [{ dir: listDir, depth: 0, rel: relDir }];
922
+ while (queue.length && results.length < MAX_RESULTS) {
923
+ const node = queue.shift();
924
+ try {
925
+ const entries = fs.readdirSync(node.dir, { withFileTypes: true });
926
+ for (const entry of entries) {
927
+ if (isIgnoredName(entry.name)) continue;
928
+ const entryAbs = path.join(node.dir, entry.name);
929
+ const entryRel = node.rel ? path.posix.join(node.rel, entry.name) : entry.name;
930
+ if (entryRel.split("/").includes("node_modules")) continue;
931
+ if (!entry.isDirectory() && isIgnoredFile(entry.name)) continue;
932
+ pushEntry(entryAbs, entryRel, entry.isDirectory());
933
+ if (entry.isDirectory() && node.depth < DEFAULT_RECURSIVE_DEPTH) {
934
+ queue.push({ dir: entryAbs, depth: node.depth + 1, rel: node.rel ? node.rel + "/" + entry.name : entry.name + "/" });
935
+ }
936
+ if (results.length >= MAX_RESULTS) break;
937
+ }
938
+ } catch (e) {
939
+ }
940
+ }
941
+ } else {
942
+ const entries = fs.readdirSync(listDir, { withFileTypes: true });
943
+ for (const entry of entries) {
944
+ if (filterPrefix && !entry.name.startsWith(filterPrefix)) continue;
945
+ if (isIgnoredName(entry.name)) continue;
946
+ if (!entry.isDirectory() && isIgnoredFile(entry.name)) continue;
947
+ const entryAbs = path.join(listDir, entry.name);
948
+ const label = relDir ? path.posix.join(relDir, entry.name) : entry.name;
949
+ pushEntry(entryAbs, label, entry.isDirectory());
950
+ if (results.length >= MAX_RESULTS) break;
951
+ }
952
+ }
1171
953
  } catch (e) {
1172
- filepath = "Error parsing arguments";
1173
954
  }
1174
- const finalFileName = getBasePath(filepath);
1175
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
1176
- /* @__PURE__ */ jsxs5(Box5, { children: [
1177
- /* @__PURE__ */ jsx5(Text5, { color: "blue", children: "\u25B8" }),
1178
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " edit " }),
1179
- /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: finalFileName })
1180
- ] }),
1181
- preview ? /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(SimpleDiff, { text: preview, maxHeight: diffMaxHeight }) }) : /* @__PURE__ */ jsx5(Box5, { paddingLeft: 2, children: /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "Generating preview..." }) })
1182
- ] });
1183
- };
1184
- var renderGeneric = ({ toolCall }) => {
1185
- const toolName = toolCall.function.name;
1186
- const rawArguments = toolCall.function.arguments;
1187
- const MAX_LINES2 = 5;
1188
- let formattedArgsString;
1189
- if (!rawArguments) {
1190
- formattedArgsString = "";
1191
- } else if (typeof rawArguments === "string") {
1192
- try {
1193
- const parsedJson = JSON.parse(rawArguments);
1194
- formattedArgsString = JSON.stringify(parsedJson, null, 2);
1195
- } catch (e) {
1196
- formattedArgsString = rawArguments;
955
+ return results.slice(0, MAX_RESULTS);
956
+ }
957
+ function useAtCompletion({
958
+ cwd,
959
+ text,
960
+ cursorPosition,
961
+ setText
962
+ }) {
963
+ const [open, setOpen] = useState(false);
964
+ const [selected, setSelected] = useState(0);
965
+ const [suggestions, setSuggestions] = useState([]);
966
+ const lastQuery = useRef2("");
967
+ function scanForAt(text2, pos) {
968
+ const before = text2.slice(0, pos);
969
+ const m = before.match(/@([\w\/.\-_]*)$/);
970
+ if (!m) return { pattern: null, insertStart: -1 };
971
+ return { pattern: m[1] || "", insertStart: m.index + 1 };
972
+ }
973
+ function update(newText, newCursor) {
974
+ const res = scanForAt(newText, newCursor);
975
+ if (res.pattern !== null && res.pattern.length >= 0) {
976
+ setOpen(true);
977
+ globalThis.__BLUMA_AT_OPEN__ = true;
978
+ const suggs = listPathSuggestions(cwd, res.pattern);
979
+ setSuggestions(suggs);
980
+ setSelected(0);
981
+ lastQuery.current = res.pattern;
982
+ } else {
983
+ setOpen(false);
984
+ globalThis.__BLUMA_AT_OPEN__ = false;
985
+ setSuggestions([]);
1197
986
  }
1198
- } else {
1199
- formattedArgsString = JSON.stringify(rawArguments, null, 2);
1200
987
  }
1201
- const lines = formattedArgsString.split("\n");
1202
- const isTruncated = lines.length > MAX_LINES2;
1203
- const visibleLines = isTruncated ? lines.slice(0, MAX_LINES2) : lines;
1204
- const remainingCount = lines.length - MAX_LINES2;
1205
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
1206
- /* @__PURE__ */ jsxs5(Box5, { children: [
1207
- /* @__PURE__ */ jsx5(Text5, { color: "blue", children: "\u25B8" }),
1208
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1209
- " ",
1210
- toolName
1211
- ] })
1212
- ] }),
1213
- formattedArgsString && /* @__PURE__ */ jsxs5(Box5, { paddingLeft: 2, flexDirection: "column", children: [
1214
- visibleLines.map((line, idx) => /* @__PURE__ */ jsx5(Text5, { color: "gray", children: line }, idx)),
1215
- isTruncated && /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1216
- "\u22EF ",
1217
- remainingCount,
1218
- " more lines"
1219
- ] })
1220
- ] })
1221
- ] });
1222
- };
1223
- var renderTodoTool = ({ toolCall }) => {
1224
- try {
1225
- const args = typeof toolCall.function.arguments === "string" ? JSON.parse(toolCall.function.arguments) : toolCall.function.arguments;
1226
- const tasks = args.tasks || [];
1227
- if (tasks.length === 0) {
1228
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
1229
- /* @__PURE__ */ jsxs5(Box5, { children: [
1230
- /* @__PURE__ */ jsx5(Text5, { color: "blue", children: "\u25B8" }),
1231
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " todo" })
1232
- ] }),
1233
- /* @__PURE__ */ jsx5(Box5, { paddingLeft: 2, children: /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Empty task list" }) })
1234
- ] });
988
+ useEffect2(() => {
989
+ update(text, cursorPosition);
990
+ }, [text, cursorPosition, cwd]);
991
+ function insertAtSelection() {
992
+ if (!open || !suggestions[selected]) return;
993
+ const res = scanForAt(text, cursorPosition);
994
+ if (!res || res.insertStart < 0) return;
995
+ let chosen = suggestions[selected].label;
996
+ const isDir = suggestions[selected].isDir;
997
+ chosen = chosen.replace(/\\/g, "/").replace(/\|/g, "");
998
+ let insertVal = chosen.replace(/\/+$/g, "");
999
+ const currentPattern = res.pattern || "";
1000
+ if (currentPattern.length > 0) {
1001
+ const normalizedPattern = currentPattern.replace(/\/+$/g, "");
1002
+ if (insertVal.startsWith(normalizedPattern)) {
1003
+ insertVal = insertVal.slice(normalizedPattern.length);
1004
+ insertVal = insertVal.replace(/^\/+/, "");
1005
+ }
1235
1006
  }
1236
- const completed = tasks.filter((t) => t.isComplete === true).length;
1237
- const pending = tasks.length - completed;
1238
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
1239
- /* @__PURE__ */ jsxs5(Box5, { children: [
1240
- /* @__PURE__ */ jsx5(Text5, { color: "blue", children: "\u25B8" }),
1241
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " todo" })
1242
- ] }),
1243
- /* @__PURE__ */ jsxs5(Box5, { paddingLeft: 2, flexDirection: "column", children: [
1244
- /* @__PURE__ */ jsxs5(Text5, { color: "magenta", children: [
1245
- "\u{1F4CB} ",
1246
- pending,
1247
- " pending, ",
1248
- completed,
1249
- " completed"
1250
- ] }),
1251
- tasks.length > 0 && tasks.length <= 10 && /* @__PURE__ */ jsx5(Box5, { paddingLeft: 2, flexDirection: "column", marginTop: 1, children: tasks.map((task, idx) => {
1252
- const isComplete = task.isComplete === true;
1253
- const checkbox = isComplete ? "[X]" : "[ ]";
1254
- const description = task.description || "No description";
1255
- const displayText = description.length > 60 ? description.substring(0, 57) + "..." : description;
1256
- const color = isComplete ? "green" : "yellow";
1257
- return /* @__PURE__ */ jsxs5(
1258
- Text5,
1259
- {
1260
- color,
1261
- strikethrough: isComplete,
1262
- dimColor: isComplete,
1263
- children: [
1007
+ insertVal = insertVal.split("\\").join("/");
1008
+ if (isDir && !insertVal.endsWith("/")) insertVal = insertVal + "/";
1009
+ const pattern = res.pattern || "";
1010
+ const lastSlash = pattern.lastIndexOf("/");
1011
+ const segmentOffset = lastSlash >= 0 ? lastSlash + 1 : 0;
1012
+ const segmentStart = res.insertStart + segmentOffset;
1013
+ const before = text.slice(0, segmentStart);
1014
+ const after = text.slice(cursorPosition);
1015
+ const newText = before + insertVal + after;
1016
+ const finalText = newText + " ";
1017
+ setText(finalText, finalText.length);
1018
+ globalThis.__BLUMA_FORCE_CURSOR_END__ = true;
1019
+ update(finalText, finalText.length);
1020
+ if (isDir) {
1021
+ setOpen(false);
1022
+ setSuggestions([]);
1023
+ setTimeout(() => {
1024
+ setOpen(true);
1025
+ update(finalText, finalText.length);
1026
+ }, 0);
1027
+ } else {
1028
+ setOpen(false);
1029
+ setSuggestions([]);
1030
+ }
1031
+ }
1032
+ function close() {
1033
+ setOpen(false);
1034
+ setSuggestions([]);
1035
+ }
1036
+ return { open, suggestions, selected, setSelected, insertAtSelection, close, update };
1037
+ }
1038
+
1039
+ // src/app/ui/components/InputPrompt.tsx
1040
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1041
+ var uiEventBus = global.__bluma_ui_eventbus__ || new EventEmitter();
1042
+ global.__bluma_ui_eventbus__ = uiEventBus;
1043
+ var TextLine = memo2(({
1044
+ line,
1045
+ lineIndex,
1046
+ cursorLine,
1047
+ cursorCol,
1048
+ showCursor
1049
+ }) => {
1050
+ const isCursorLine = lineIndex === cursorLine;
1051
+ if (!isCursorLine || !showCursor) {
1052
+ return /* @__PURE__ */ jsx2(Text2, { children: line });
1053
+ }
1054
+ const before = line.slice(0, cursorCol);
1055
+ const char = line[cursorCol] || " ";
1056
+ const after = line.slice(cursorCol + 1);
1057
+ return /* @__PURE__ */ jsxs2(Text2, { children: [
1058
+ before,
1059
+ /* @__PURE__ */ jsx2(Text2, { inverse: true, color: "white", children: char }),
1060
+ after
1061
+ ] });
1062
+ }, (prev, next) => {
1063
+ if (prev.line !== next.line) return false;
1064
+ if (prev.showCursor !== next.showCursor) return false;
1065
+ const prevIsCursorLine = prev.lineIndex === prev.cursorLine;
1066
+ const nextIsCursorLine = next.lineIndex === next.cursorLine;
1067
+ if (prevIsCursorLine !== nextIsCursorLine) return false;
1068
+ if (nextIsCursorLine && prev.cursorCol !== next.cursorCol) return false;
1069
+ return true;
1070
+ });
1071
+ TextLine.displayName = "TextLine";
1072
+ var PathSuggestions = memo2(({
1073
+ suggestions,
1074
+ selected
1075
+ }) => {
1076
+ const VISIBLE = 7;
1077
+ const total = suggestions.length;
1078
+ const sel = Math.max(0, Math.min(selected, total - 1));
1079
+ let start = Math.max(0, sel - Math.floor(VISIBLE / 2));
1080
+ if (start + VISIBLE > total) start = Math.max(0, total - VISIBLE);
1081
+ const windowItems = suggestions.slice(start, start + VISIBLE);
1082
+ return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", marginTop: 1, height: Math.min(VISIBLE, total), children: windowItems.map((s, idx) => {
1083
+ const realIdx = start + idx;
1084
+ const isSelected = realIdx === selected;
1085
+ return /* @__PURE__ */ jsxs2(Box2, { paddingLeft: 1, paddingY: 0, children: [
1086
+ /* @__PURE__ */ jsx2(Text2, { color: isSelected ? "magenta" : "gray", children: isSelected ? "\u276F " : " " }),
1087
+ /* @__PURE__ */ jsx2(Text2, { color: isSelected ? "magenta" : "white", bold: isSelected, dimColor: !isSelected, children: s.label })
1088
+ ] }, s.fullPath);
1089
+ }) });
1090
+ });
1091
+ PathSuggestions.displayName = "PathSuggestions";
1092
+ var SlashSuggestions = memo2(({
1093
+ suggestions,
1094
+ selectedIndex
1095
+ }) => /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", marginTop: 1, children: suggestions.map((s, idx) => {
1096
+ const isSelected = idx === selectedIndex;
1097
+ return /* @__PURE__ */ jsxs2(Box2, { paddingLeft: 1, paddingY: 0, children: [
1098
+ /* @__PURE__ */ jsx2(Text2, { color: isSelected ? "magenta" : "gray", children: isSelected ? "\u276F " : " " }),
1099
+ /* @__PURE__ */ jsxs2(Text2, { color: isSelected ? "magenta" : "white", bold: isSelected, dimColor: !isSelected, children: [
1100
+ s.name,
1101
+ " ",
1102
+ /* @__PURE__ */ jsxs2(Text2, { color: "gray", children: [
1103
+ "- ",
1104
+ s.description
1105
+ ] })
1106
+ ] })
1107
+ ] }, s.name);
1108
+ }) }));
1109
+ SlashSuggestions.displayName = "SlashSuggestions";
1110
+ var Footer = memo2(({ isReadOnly }) => /* @__PURE__ */ jsx2(Box2, { paddingX: 1, justifyContent: "center", children: /* @__PURE__ */ jsx2(Text2, { color: "gray", dimColor: true, children: isReadOnly ? "ctrl+c to exit | Enter to send message | Shift+Enter for new line | esc interrupt" : "ctrl+c to exit | Enter to submit | Shift+Enter for new line | /help commands | esc interrupt" }) }));
1111
+ Footer.displayName = "Footer";
1112
+ var TextLinesRenderer = memo2(({
1113
+ lines,
1114
+ cursorLine,
1115
+ cursorCol,
1116
+ showCursor,
1117
+ showPlaceholder,
1118
+ placeholder
1119
+ }) => {
1120
+ return /* @__PURE__ */ jsx2(Fragment2, { children: lines.map((line, idx) => {
1121
+ const isFirstLine = idx === 0;
1122
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", paddingX: 1, children: [
1123
+ isFirstLine && /* @__PURE__ */ jsxs2(Text2, { color: "white", children: [
1124
+ ">",
1125
+ " "
1126
+ ] }),
1127
+ !isFirstLine && /* @__PURE__ */ jsxs2(Text2, { color: "gray", children: [
1128
+ "\u2502",
1129
+ " "
1130
+ ] }),
1131
+ showPlaceholder && isFirstLine && line.length === 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: placeholder }) : /* @__PURE__ */ jsx2(
1132
+ TextLine,
1133
+ {
1134
+ line,
1135
+ lineIndex: idx,
1136
+ cursorLine,
1137
+ cursorCol,
1138
+ showCursor
1139
+ }
1140
+ )
1141
+ ] }, idx);
1142
+ }) });
1143
+ });
1144
+ TextLinesRenderer.displayName = "TextLinesRenderer";
1145
+ var InputPrompt = memo2(({
1146
+ onSubmit,
1147
+ isReadOnly,
1148
+ onInterrupt,
1149
+ disableWhileProcessing = false
1150
+ }) => {
1151
+ const { stdout } = useStdout();
1152
+ const [viewWidth] = useState2(() => stdout.columns - 6);
1153
+ const [slashOpen, setSlashOpen] = useState2(false);
1154
+ const [slashIndex, setSlashIndex] = useState2(0);
1155
+ const permissiveOnSubmit = (value) => {
1156
+ const trimmed = (value || "").trim();
1157
+ if (isReadOnly) {
1158
+ if (trimmed.length > 0) {
1159
+ uiEventBus.emit("user_overlay", { kind: "message", payload: trimmed, ts: Date.now() });
1160
+ }
1161
+ return;
1162
+ }
1163
+ onSubmit(value);
1164
+ };
1165
+ const { text, cursorPosition, setText } = useCustomInput({
1166
+ onSubmit: (value) => {
1167
+ if (disableWhileProcessing && isReadOnly) return;
1168
+ if (pathAutocomplete.open) return;
1169
+ permissiveOnSubmit(value);
1170
+ },
1171
+ viewWidth,
1172
+ isReadOnly,
1173
+ onInterrupt
1174
+ });
1175
+ const linesData = useMemo(() => {
1176
+ const lines = text.split("\n");
1177
+ let remainingChars = cursorPosition;
1178
+ let cursorLine = 0;
1179
+ let cursorCol = 0;
1180
+ for (let i = 0; i < lines.length; i++) {
1181
+ const lineLength = lines[i].length;
1182
+ if (remainingChars <= lineLength) {
1183
+ cursorLine = i;
1184
+ cursorCol = remainingChars;
1185
+ break;
1186
+ }
1187
+ remainingChars -= lineLength + 1;
1188
+ if (i === lines.length - 1) {
1189
+ cursorLine = i;
1190
+ cursorCol = lineLength;
1191
+ }
1192
+ }
1193
+ return { lines, cursorLine, cursorCol, totalLines: lines.length };
1194
+ }, [text, cursorPosition]);
1195
+ const displayData = linesData;
1196
+ const placeholder = isReadOnly ? " Press Esc to cancel | Enter message while agent runs" : "";
1197
+ const showPlaceholder = text.length === 0 && isReadOnly;
1198
+ const slashQuery = useMemo(() => text.startsWith("/") ? text : "", [text]);
1199
+ const slashSuggestions = useMemo(() => {
1200
+ if (!slashQuery) return [];
1201
+ return filterSlashCommands(slashQuery);
1202
+ }, [slashQuery]);
1203
+ useEffect3(() => {
1204
+ if (isReadOnly) {
1205
+ setSlashOpen(false);
1206
+ return;
1207
+ }
1208
+ if (text.startsWith("/")) {
1209
+ setSlashOpen(true);
1210
+ setSlashIndex(0);
1211
+ } else {
1212
+ setSlashOpen(false);
1213
+ }
1214
+ }, [text, isReadOnly]);
1215
+ useInput2((input, key) => {
1216
+ if (!slashOpen) return;
1217
+ if (key.downArrow) {
1218
+ setSlashIndex((i) => Math.min(i + 1, Math.max(0, slashSuggestions.length - 1)));
1219
+ } else if (key.upArrow) {
1220
+ setSlashIndex((i) => Math.max(i - 1, 0));
1221
+ } else if (key.return) {
1222
+ const choice = slashSuggestions[slashIndex];
1223
+ if (choice) {
1224
+ setSlashOpen(false);
1225
+ try {
1226
+ setText(`${choice.name} `);
1227
+ } catch (e) {
1228
+ permissiveOnSubmit(`${choice.name} `);
1229
+ }
1230
+ }
1231
+ } else if (key.escape) {
1232
+ setSlashOpen(false);
1233
+ }
1234
+ }, { isActive: slashOpen });
1235
+ useEffect3(() => {
1236
+ if (globalThis.__BLUMA_FORCE_CURSOR_END__) {
1237
+ setText(text, text.length);
1238
+ delete globalThis.__BLUMA_FORCE_CURSOR_END__;
1239
+ }
1240
+ }, [text, setText]);
1241
+ const cwd = process.cwd();
1242
+ const pathAutocomplete = useAtCompletion({ cwd, text, cursorPosition, setText });
1243
+ useInput2((input, key) => {
1244
+ if (key.backspace || key.delete || key.ctrl || key.meta) return;
1245
+ if (pathAutocomplete.open) {
1246
+ if (key.downArrow) {
1247
+ pathAutocomplete.setSelected((i) => Math.min(i + 1, Math.max(0, pathAutocomplete.suggestions.length - 1)));
1248
+ return;
1249
+ } else if (key.upArrow) {
1250
+ pathAutocomplete.setSelected((i) => Math.max(i - 1, 0));
1251
+ return;
1252
+ } else if (key.return || key.tab) {
1253
+ const selected = pathAutocomplete.suggestions[pathAutocomplete.selected];
1254
+ if (selected) {
1255
+ const before = text.slice(0, cursorPosition);
1256
+ const m = before.match(/@([\w\/.\-_]*)$/);
1257
+ if (m) {
1258
+ globalThis.__BLUMA_SUPPRESS_SUBMIT__ = true;
1259
+ pathAutocomplete.insertAtSelection();
1260
+ return;
1261
+ }
1262
+ }
1263
+ return;
1264
+ } else if (key.escape) {
1265
+ pathAutocomplete.close();
1266
+ return;
1267
+ }
1268
+ return;
1269
+ }
1270
+ }, { isActive: true });
1271
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
1272
+ disableWhileProcessing ? /* @__PURE__ */ jsx2(Box2, { children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", paddingX: 1, flexWrap: "nowrap", children: [
1273
+ /* @__PURE__ */ jsxs2(Text2, { color: "white", children: [
1274
+ ">",
1275
+ " "
1276
+ ] }),
1277
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "ctrl+c to exit" })
1278
+ ] }) }) : /* @__PURE__ */ jsxs2(Fragment2, { children: [
1279
+ /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: /* @__PURE__ */ jsx2(
1280
+ TextLinesRenderer,
1281
+ {
1282
+ lines: displayData.lines,
1283
+ cursorLine: displayData.cursorLine,
1284
+ cursorCol: displayData.cursorCol,
1285
+ showCursor: !isReadOnly,
1286
+ showPlaceholder,
1287
+ placeholder
1288
+ }
1289
+ ) }),
1290
+ pathAutocomplete.open && pathAutocomplete.suggestions.length > 0 && /* @__PURE__ */ jsx2(
1291
+ PathSuggestions,
1292
+ {
1293
+ suggestions: pathAutocomplete.suggestions,
1294
+ selected: pathAutocomplete.selected
1295
+ }
1296
+ ),
1297
+ slashOpen && slashSuggestions.length > 0 && /* @__PURE__ */ jsx2(
1298
+ SlashSuggestions,
1299
+ {
1300
+ suggestions: slashSuggestions,
1301
+ selectedIndex: slashIndex
1302
+ }
1303
+ )
1304
+ ] }),
1305
+ /* @__PURE__ */ jsx2(Footer, { isReadOnly })
1306
+ ] });
1307
+ });
1308
+ InputPrompt.displayName = "InputPrompt";
1309
+
1310
+ // src/app/ui/ConfirmationPrompt.tsx
1311
+ import { memo as memo4 } from "react";
1312
+ import { Box as Box6, Text as Text6 } from "ink";
1313
+
1314
+ // src/app/ui/InteractiveMenu.tsx
1315
+ import { useState as useState3, memo as memo3 } from "react";
1316
+ import { Box as Box3, Text as Text3, useInput as useInput3 } from "ink";
1317
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1318
+ var InteractiveMenuComponent = ({ onDecision }) => {
1319
+ const options = [
1320
+ { key: "y", label: "yes", value: "accept", color: "green" },
1321
+ { key: "n", label: "no", value: "decline", color: "red" },
1322
+ { key: "a", label: "always", value: "accept_always", color: "yellow" }
1323
+ ];
1324
+ const [selected, setSelected] = useState3(0);
1325
+ useInput3((input, key) => {
1326
+ if (key.upArrow || key.leftArrow) {
1327
+ setSelected((i) => i > 0 ? i - 1 : options.length - 1);
1328
+ }
1329
+ if (key.downArrow || key.rightArrow) {
1330
+ setSelected((i) => i < options.length - 1 ? i + 1 : 0);
1331
+ }
1332
+ if (key.escape) {
1333
+ onDecision("decline");
1334
+ }
1335
+ if (key.return) {
1336
+ onDecision(options[selected].value);
1337
+ }
1338
+ const opt = options.find((o) => o.key === input.toLowerCase());
1339
+ if (opt) {
1340
+ onDecision(opt.value);
1341
+ }
1342
+ });
1343
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 1, marginTop: 1, children: [
1344
+ /* @__PURE__ */ jsxs3(Box3, { children: [
1345
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "approve? " }),
1346
+ options.map((opt, idx) => {
1347
+ const isSelected = idx === selected;
1348
+ return /* @__PURE__ */ jsx3(Box3, { marginRight: 1, children: /* @__PURE__ */ jsxs3(Text3, { color: isSelected ? opt.color : "gray", bold: isSelected, children: [
1349
+ "[",
1350
+ opt.key,
1351
+ "]",
1352
+ opt.label
1353
+ ] }) }, opt.value);
1354
+ })
1355
+ ] }),
1356
+ /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "arrows to select, enter to confirm, esc to cancel" }) })
1357
+ ] });
1358
+ };
1359
+ var InteractiveMenu = memo3(InteractiveMenuComponent);
1360
+
1361
+ // src/app/ui/components/promptRenderers.tsx
1362
+ import { Box as Box5, Text as Text5 } from "ink";
1363
+ import path2 from "path";
1364
+
1365
+ // src/app/ui/components/SimpleDiff.tsx
1366
+ import { Box as Box4, Text as Text4 } from "ink";
1367
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1368
+ var SimpleDiff = ({ text, maxHeight }) => {
1369
+ const allLines = (text || "").split("\n").filter((line) => line !== "");
1370
+ const isTruncated = maxHeight > 0 && allLines.length > maxHeight;
1371
+ const linesToRender = isTruncated ? allLines.slice(-maxHeight) : allLines;
1372
+ const hiddenCount = allLines.length - linesToRender.length;
1373
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", paddingX: 2, children: [
1374
+ isTruncated && /* @__PURE__ */ jsx4(Box4, { marginBottom: 0, children: /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1375
+ "\u22EF ",
1376
+ hiddenCount,
1377
+ " lines hidden"
1378
+ ] }) }),
1379
+ linesToRender.map((line, index) => {
1380
+ if (line.startsWith("---") || line.startsWith("+++")) {
1381
+ return null;
1382
+ }
1383
+ let color = "white";
1384
+ let prefix = "";
1385
+ if (line.startsWith("+")) {
1386
+ color = "green";
1387
+ prefix = "+ ";
1388
+ } else if (line.startsWith("-")) {
1389
+ color = "red";
1390
+ prefix = "- ";
1391
+ } else if (line.startsWith("@@")) {
1392
+ color = "cyan";
1393
+ prefix = "";
1394
+ } else {
1395
+ color = "gray";
1396
+ prefix = " ";
1397
+ }
1398
+ return /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs4(Text4, { color, children: [
1399
+ prefix,
1400
+ line.replace(/^[+\-]/, "")
1401
+ ] }) }, index);
1402
+ })
1403
+ ] });
1404
+ };
1405
+
1406
+ // src/app/ui/components/promptRenderers.tsx
1407
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1408
+ var getBasePath = (filePath) => {
1409
+ return path2.basename(filePath);
1410
+ };
1411
+ var renderShellCommand = ({ toolCall }) => {
1412
+ let command = "";
1413
+ try {
1414
+ const args = typeof toolCall.function.arguments === "string" ? JSON.parse(toolCall.function.arguments) : toolCall.function.arguments;
1415
+ command = args.command || "[command not found]";
1416
+ } catch (e) {
1417
+ command = "Error parsing command";
1418
+ }
1419
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
1420
+ /* @__PURE__ */ jsxs5(Box5, { children: [
1421
+ /* @__PURE__ */ jsx5(Text5, { color: "blue", children: "\u25B8" }),
1422
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " shell" })
1423
+ ] }),
1424
+ /* @__PURE__ */ jsx5(Box5, { paddingLeft: 2, children: /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: command }) })
1425
+ ] });
1426
+ };
1427
+ var renderLsTool = ({ toolCall }) => {
1428
+ let directoryPath = "[path not specified]";
1429
+ try {
1430
+ const args = typeof toolCall.function.arguments === "string" ? JSON.parse(toolCall.function.arguments) : toolCall.function.arguments;
1431
+ directoryPath = args.directory_path || "[path not specified]";
1432
+ } catch (e) {
1433
+ directoryPath = "Error parsing arguments";
1434
+ }
1435
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
1436
+ /* @__PURE__ */ jsxs5(Box5, { children: [
1437
+ /* @__PURE__ */ jsx5(Text5, { color: "blue", children: "\u25B8" }),
1438
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " ls" })
1439
+ ] }),
1440
+ /* @__PURE__ */ jsx5(Box5, { paddingLeft: 2, children: /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: directoryPath }) })
1441
+ ] });
1442
+ };
1443
+ var renderCountFilesLinesTool = ({ toolCall }) => {
1444
+ let filepath = "[path not specified]";
1445
+ try {
1446
+ const args = typeof toolCall.function.arguments === "string" ? JSON.parse(toolCall.function.arguments) : toolCall.function.arguments;
1447
+ filepath = args.filepath || "[path not specified]";
1448
+ } catch (e) {
1449
+ filepath = "Error parsing arguments";
1450
+ }
1451
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
1452
+ /* @__PURE__ */ jsxs5(Box5, { children: [
1453
+ /* @__PURE__ */ jsx5(Text5, { color: "blue", children: "\u25B8" }),
1454
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " count lines" })
1455
+ ] }),
1456
+ /* @__PURE__ */ jsx5(Box5, { paddingLeft: 2, children: /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: filepath }) })
1457
+ ] });
1458
+ };
1459
+ var renderReadFileLines = ({ toolCall }) => {
1460
+ let filepath = "[path not specified]";
1461
+ let startLine = 0;
1462
+ let endLine = 0;
1463
+ try {
1464
+ const args = typeof toolCall.function.arguments === "string" ? JSON.parse(toolCall.function.arguments) : toolCall.function.arguments;
1465
+ filepath = args.filepath || "[path not specified]";
1466
+ startLine = args.start_line || 0;
1467
+ endLine = args.end_line || 0;
1468
+ } catch (e) {
1469
+ filepath = "Error parsing arguments";
1470
+ }
1471
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
1472
+ /* @__PURE__ */ jsxs5(Box5, { children: [
1473
+ /* @__PURE__ */ jsx5(Text5, { color: "blue", children: "\u25B8" }),
1474
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " read" })
1475
+ ] }),
1476
+ /* @__PURE__ */ jsxs5(Box5, { paddingLeft: 2, flexDirection: "column", children: [
1477
+ /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: filepath }),
1478
+ /* @__PURE__ */ jsxs5(Box5, { paddingLeft: 2, children: [
1479
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "lines " }),
1480
+ /* @__PURE__ */ jsx5(Text5, { color: "magenta", children: startLine }),
1481
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " to " }),
1482
+ /* @__PURE__ */ jsx5(Text5, { color: "magenta", children: endLine })
1483
+ ] })
1484
+ ] })
1485
+ ] });
1486
+ };
1487
+ var renderEditTool = ({ toolCall, preview }) => {
1488
+ const diffMaxHeight = 5;
1489
+ let filepath = "[path not specified]";
1490
+ try {
1491
+ const args = JSON.parse(toolCall.function.arguments);
1492
+ filepath = args.file_path || "[path not specified]";
1493
+ } catch (e) {
1494
+ filepath = "Error parsing arguments";
1495
+ }
1496
+ const finalFileName = getBasePath(filepath);
1497
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
1498
+ /* @__PURE__ */ jsxs5(Box5, { children: [
1499
+ /* @__PURE__ */ jsx5(Text5, { color: "blue", children: "\u25B8" }),
1500
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " edit " }),
1501
+ /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: finalFileName })
1502
+ ] }),
1503
+ preview ? /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(SimpleDiff, { text: preview, maxHeight: diffMaxHeight }) }) : /* @__PURE__ */ jsx5(Box5, { paddingLeft: 2, children: /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "Generating preview..." }) })
1504
+ ] });
1505
+ };
1506
+ var renderGeneric = ({ toolCall }) => {
1507
+ const toolName = toolCall.function.name;
1508
+ const rawArguments = toolCall.function.arguments;
1509
+ const MAX_LINES2 = 5;
1510
+ let formattedArgsString;
1511
+ if (!rawArguments) {
1512
+ formattedArgsString = "";
1513
+ } else if (typeof rawArguments === "string") {
1514
+ try {
1515
+ const parsedJson = JSON.parse(rawArguments);
1516
+ formattedArgsString = JSON.stringify(parsedJson, null, 2);
1517
+ } catch (e) {
1518
+ formattedArgsString = rawArguments;
1519
+ }
1520
+ } else {
1521
+ formattedArgsString = JSON.stringify(rawArguments, null, 2);
1522
+ }
1523
+ const lines = formattedArgsString.split("\n");
1524
+ const isTruncated = lines.length > MAX_LINES2;
1525
+ const visibleLines = isTruncated ? lines.slice(0, MAX_LINES2) : lines;
1526
+ const remainingCount = lines.length - MAX_LINES2;
1527
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
1528
+ /* @__PURE__ */ jsxs5(Box5, { children: [
1529
+ /* @__PURE__ */ jsx5(Text5, { color: "blue", children: "\u25B8" }),
1530
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1531
+ " ",
1532
+ toolName
1533
+ ] })
1534
+ ] }),
1535
+ formattedArgsString && /* @__PURE__ */ jsxs5(Box5, { paddingLeft: 2, flexDirection: "column", children: [
1536
+ visibleLines.map((line, idx) => /* @__PURE__ */ jsx5(Text5, { color: "gray", children: line }, idx)),
1537
+ isTruncated && /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1538
+ "\u22EF ",
1539
+ remainingCount,
1540
+ " more lines"
1541
+ ] })
1542
+ ] })
1543
+ ] });
1544
+ };
1545
+ var renderTodoTool = ({ toolCall }) => {
1546
+ try {
1547
+ const args = typeof toolCall.function.arguments === "string" ? JSON.parse(toolCall.function.arguments) : toolCall.function.arguments;
1548
+ const tasks = args.tasks || [];
1549
+ if (tasks.length === 0) {
1550
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
1551
+ /* @__PURE__ */ jsxs5(Box5, { children: [
1552
+ /* @__PURE__ */ jsx5(Text5, { color: "blue", children: "\u25B8" }),
1553
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " todo" })
1554
+ ] }),
1555
+ /* @__PURE__ */ jsx5(Box5, { paddingLeft: 2, children: /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Empty task list" }) })
1556
+ ] });
1557
+ }
1558
+ const completed = tasks.filter((t) => t.isComplete === true).length;
1559
+ const pending = tasks.length - completed;
1560
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
1561
+ /* @__PURE__ */ jsxs5(Box5, { children: [
1562
+ /* @__PURE__ */ jsx5(Text5, { color: "blue", children: "\u25B8" }),
1563
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " todo" })
1564
+ ] }),
1565
+ /* @__PURE__ */ jsxs5(Box5, { paddingLeft: 2, flexDirection: "column", children: [
1566
+ /* @__PURE__ */ jsxs5(Text5, { color: "magenta", children: [
1567
+ "\u{1F4CB} ",
1568
+ pending,
1569
+ " pending, ",
1570
+ completed,
1571
+ " completed"
1572
+ ] }),
1573
+ tasks.length > 0 && tasks.length <= 10 && /* @__PURE__ */ jsx5(Box5, { paddingLeft: 2, flexDirection: "column", marginTop: 1, children: tasks.map((task, idx) => {
1574
+ const isComplete = task.isComplete === true;
1575
+ const checkbox = isComplete ? "[X]" : "[ ]";
1576
+ const description = task.description || "No description";
1577
+ const displayText = description.length > 60 ? description.substring(0, 57) + "..." : description;
1578
+ const color = isComplete ? "green" : "yellow";
1579
+ return /* @__PURE__ */ jsxs5(
1580
+ Text5,
1581
+ {
1582
+ color,
1583
+ strikethrough: isComplete,
1584
+ dimColor: isComplete,
1585
+ children: [
1264
1586
  checkbox,
1265
1587
  " ",
1266
1588
  displayText
@@ -1919,1144 +2241,843 @@ function addTasks(tasks) {
1919
2241
  taskStore.push(newTask);
1920
2242
  }
1921
2243
  saveTasksToFile();
1922
- return createResult(true, `Added ${tasks.length} task(s)`);
1923
- }
1924
- function completeTask(taskId) {
1925
- loadTasksFromFile();
1926
- const task = taskStore.find((t) => t.id === taskId);
1927
- if (!task) {
1928
- return createResult(false, `Task #${taskId} not found`);
1929
- }
1930
- task.status = "completed";
1931
- task.completedAt = (/* @__PURE__ */ new Date()).toISOString();
1932
- saveTasksToFile();
1933
- return createResult(true, `Completed: ${task.description}`);
1934
- }
1935
- function updateTask(taskId, description, priority) {
1936
- loadTasksFromFile();
1937
- const task = taskStore.find((t) => t.id === taskId);
1938
- if (!task) {
1939
- return createResult(false, `Task #${taskId} not found`);
1940
- }
1941
- if (description) {
1942
- const error = validateDescription(description);
1943
- if (error) {
1944
- return createResult(false, error);
1945
- }
1946
- task.description = description.trim();
1947
- }
1948
- if (priority) {
1949
- task.priority = priority;
1950
- }
1951
- saveTasksToFile();
1952
- return createResult(true, `Updated task #${taskId}`);
1953
- }
1954
- function removeTask(taskId) {
1955
- loadTasksFromFile();
1956
- const index = taskStore.findIndex((t) => t.id === taskId);
1957
- if (index === -1) {
1958
- return createResult(false, `Task #${taskId} not found`);
1959
- }
1960
- const removed = taskStore.splice(index, 1)[0];
1961
- saveTasksToFile();
1962
- return createResult(true, `Removed: ${removed.description}`);
1963
- }
1964
- function clearCompleted() {
1965
- loadTasksFromFile();
1966
- const before = taskStore.length;
1967
- taskStore = taskStore.filter((t) => t.status !== "completed");
1968
- const removed = before - taskStore.length;
1969
- saveTasksToFile();
1970
- return createResult(true, `Cleared ${removed} completed task(s)`);
1971
- }
1972
- function generateProgressBar(percent) {
1973
- const width = 10;
1974
- const filled = Math.round(percent / 100 * width);
1975
- const empty = width - filled;
1976
- return "[" + "=".repeat(filled) + " ".repeat(empty) + "]";
1977
- }
1978
- async function todo(args) {
1979
- if ("tasks" in args && !("action" in args)) {
1980
- loadTasksFromFile();
1981
- taskStore = [];
1982
- nextId = 1;
1983
- if (args.tasks && args.tasks.length > 0) {
1984
- for (const task of args.tasks) {
1985
- taskStore.push({
1986
- id: nextId++,
1987
- description: task.description,
1988
- status: task.isComplete ? "completed" : "pending",
1989
- priority: "medium",
1990
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1991
- completedAt: task.isComplete ? (/* @__PURE__ */ new Date()).toISOString() : void 0
1992
- });
1993
- }
1994
- }
1995
- saveTasksToFile();
1996
- const stats = calculateStats();
1997
- const progressBar = generateProgressBar(stats.progress);
1998
- return createResult(true, `Synced ${taskStore.length} tasks ${progressBar} ${stats.progress}%`);
1999
- }
2000
- const fullArgs = args;
2001
- switch (fullArgs.action) {
2002
- case "list":
2003
- return listTasks();
2004
- case "add":
2005
- return addTasks(fullArgs.tasks);
2006
- case "complete":
2007
- if (!fullArgs.taskId) {
2008
- return createResult(false, "taskId required for complete action");
2009
- }
2010
- return completeTask(fullArgs.taskId);
2011
- case "update":
2012
- if (!fullArgs.taskId) {
2013
- return createResult(false, "taskId required for update action");
2014
- }
2015
- return updateTask(fullArgs.taskId, fullArgs.description, fullArgs.priority);
2016
- case "remove":
2017
- if (!fullArgs.taskId) {
2018
- return createResult(false, "taskId required for remove action");
2019
- }
2020
- return removeTask(fullArgs.taskId);
2021
- case "clear":
2022
- return clearCompleted();
2023
- case "sync":
2024
- return addTasks(fullArgs.tasks);
2025
- default:
2026
- return createResult(false, `Unknown action: ${fullArgs.action}`);
2027
- }
2028
- }
2029
-
2030
- // src/app/agent/tools/natives/find_by_name.ts
2031
- import path6 from "path";
2032
- import { promises as fsPromises } from "fs";
2033
- var DEFAULT_IGNORE_PATTERNS = [
2034
- "node_modules",
2035
- ".git",
2036
- ".venv",
2037
- "venv",
2038
- "__pycache__",
2039
- ".cache",
2040
- "dist",
2041
- "build",
2042
- ".next",
2043
- ".nuxt",
2044
- "coverage",
2045
- ".nyc_output",
2046
- ".pytest_cache",
2047
- "target",
2048
- // Rust
2049
- "vendor"
2050
- // Go, PHP
2051
- ];
2052
- var MAX_RESULTS2 = 100;
2053
- var MAX_DEPTH_DEFAULT = 10;
2054
- function globToRegex(glob) {
2055
- let regex = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "{{DOUBLESTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, ".").replace(/{{DOUBLESTAR}}/g, ".*");
2056
- return new RegExp(`^${regex}$`, "i");
2057
- }
2058
- function shouldIgnore(name, ignorePatterns, includeHidden) {
2059
- if (!includeHidden && name.startsWith(".")) {
2060
- return true;
2061
- }
2062
- for (const pattern of ignorePatterns) {
2063
- if (name === pattern || name.match(globToRegex(pattern))) {
2064
- return true;
2065
- }
2066
- }
2067
- return false;
2068
- }
2069
- function matchesExtensions(filename, extensions) {
2070
- if (!extensions || extensions.length === 0) return true;
2071
- const ext = path6.extname(filename).toLowerCase();
2072
- return extensions.some((e) => {
2073
- const normalizedExt = e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`;
2074
- return ext === normalizedExt;
2075
- });
2076
- }
2077
- async function searchDirectory(dir, pattern, baseDir, options, results) {
2078
- if (options.currentDepth > options.maxDepth || results.length >= MAX_RESULTS2) {
2079
- return;
2080
- }
2081
- let entries;
2082
- try {
2083
- entries = await fsPromises.readdir(dir, { withFileTypes: true });
2084
- } catch (error) {
2085
- return;
2086
- }
2087
- for (const entry of entries) {
2088
- if (results.length >= MAX_RESULTS2) break;
2089
- const name = entry.name;
2090
- if (shouldIgnore(name, options.ignorePatterns, options.includeHidden)) {
2091
- continue;
2092
- }
2093
- const fullPath = path6.join(dir, name);
2094
- const relativePath = path6.relative(baseDir, fullPath);
2095
- if (entry.isDirectory()) {
2096
- if (pattern.test(name)) {
2097
- results.push({
2098
- path: fullPath,
2099
- relative_path: relativePath,
2100
- type: "directory"
2101
- });
2102
- }
2103
- await searchDirectory(fullPath, pattern, baseDir, {
2104
- ...options,
2105
- currentDepth: options.currentDepth + 1
2106
- }, results);
2107
- } else if (entry.isFile()) {
2108
- if (pattern.test(name) && matchesExtensions(name, options.extensions)) {
2109
- try {
2110
- const stats = await fsPromises.stat(fullPath);
2111
- results.push({
2112
- path: fullPath,
2113
- relative_path: relativePath,
2114
- type: "file",
2115
- size: stats.size,
2116
- modified: stats.mtime.toISOString()
2117
- });
2118
- } catch {
2119
- results.push({
2120
- path: fullPath,
2121
- relative_path: relativePath,
2122
- type: "file"
2123
- });
2124
- }
2125
- }
2126
- }
2244
+ return createResult(true, `Added ${tasks.length} task(s)`);
2245
+ }
2246
+ function completeTask(taskId) {
2247
+ loadTasksFromFile();
2248
+ const task = taskStore.find((t) => t.id === taskId);
2249
+ if (!task) {
2250
+ return createResult(false, `Task #${taskId} not found`);
2127
2251
  }
2252
+ task.status = "completed";
2253
+ task.completedAt = (/* @__PURE__ */ new Date()).toISOString();
2254
+ saveTasksToFile();
2255
+ return createResult(true, `Completed: ${task.description}`);
2128
2256
  }
2129
- async function findByName(args) {
2130
- try {
2131
- const {
2132
- pattern,
2133
- directory = process.cwd(),
2134
- extensions,
2135
- max_depth = MAX_DEPTH_DEFAULT,
2136
- include_hidden = false,
2137
- exclude_patterns = []
2138
- } = args;
2139
- if (!pattern || typeof pattern !== "string") {
2140
- return {
2141
- success: false,
2142
- pattern: pattern || "",
2143
- directory,
2144
- results: [],
2145
- total_found: 0,
2146
- truncated: false,
2147
- error: "Pattern is required and must be a string"
2148
- };
2257
+ function updateTask(taskId, description, priority) {
2258
+ loadTasksFromFile();
2259
+ const task = taskStore.find((t) => t.id === taskId);
2260
+ if (!task) {
2261
+ return createResult(false, `Task #${taskId} not found`);
2262
+ }
2263
+ if (description) {
2264
+ const error = validateDescription(description);
2265
+ if (error) {
2266
+ return createResult(false, error);
2149
2267
  }
2150
- const resolvedDir = path6.resolve(directory);
2151
- try {
2152
- const stats = await fsPromises.stat(resolvedDir);
2153
- if (!stats.isDirectory()) {
2154
- return {
2155
- success: false,
2156
- pattern,
2157
- directory: resolvedDir,
2158
- results: [],
2159
- total_found: 0,
2160
- truncated: false,
2161
- error: `Path is not a directory: ${resolvedDir}`
2162
- };
2268
+ task.description = description.trim();
2269
+ }
2270
+ if (priority) {
2271
+ task.priority = priority;
2272
+ }
2273
+ saveTasksToFile();
2274
+ return createResult(true, `Updated task #${taskId}`);
2275
+ }
2276
+ function removeTask(taskId) {
2277
+ loadTasksFromFile();
2278
+ const index = taskStore.findIndex((t) => t.id === taskId);
2279
+ if (index === -1) {
2280
+ return createResult(false, `Task #${taskId} not found`);
2281
+ }
2282
+ const removed = taskStore.splice(index, 1)[0];
2283
+ saveTasksToFile();
2284
+ return createResult(true, `Removed: ${removed.description}`);
2285
+ }
2286
+ function clearCompleted() {
2287
+ loadTasksFromFile();
2288
+ const before = taskStore.length;
2289
+ taskStore = taskStore.filter((t) => t.status !== "completed");
2290
+ const removed = before - taskStore.length;
2291
+ saveTasksToFile();
2292
+ return createResult(true, `Cleared ${removed} completed task(s)`);
2293
+ }
2294
+ function generateProgressBar(percent) {
2295
+ const width = 10;
2296
+ const filled = Math.round(percent / 100 * width);
2297
+ const empty = width - filled;
2298
+ return "[" + "=".repeat(filled) + " ".repeat(empty) + "]";
2299
+ }
2300
+ async function todo(args) {
2301
+ if ("tasks" in args && !("action" in args)) {
2302
+ loadTasksFromFile();
2303
+ taskStore = [];
2304
+ nextId = 1;
2305
+ if (args.tasks && args.tasks.length > 0) {
2306
+ for (const task of args.tasks) {
2307
+ taskStore.push({
2308
+ id: nextId++,
2309
+ description: task.description,
2310
+ status: task.isComplete ? "completed" : "pending",
2311
+ priority: "medium",
2312
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2313
+ completedAt: task.isComplete ? (/* @__PURE__ */ new Date()).toISOString() : void 0
2314
+ });
2163
2315
  }
2164
- } catch (error) {
2165
- return {
2166
- success: false,
2167
- pattern,
2168
- directory: resolvedDir,
2169
- results: [],
2170
- total_found: 0,
2171
- truncated: false,
2172
- error: `Directory not found: ${resolvedDir}`
2173
- };
2174
2316
  }
2175
- const ignorePatterns = [...DEFAULT_IGNORE_PATTERNS, ...exclude_patterns];
2176
- const patternRegex = globToRegex(pattern);
2177
- const results = [];
2178
- await searchDirectory(resolvedDir, patternRegex, resolvedDir, {
2179
- extensions,
2180
- maxDepth: max_depth,
2181
- currentDepth: 0,
2182
- includeHidden: include_hidden,
2183
- ignorePatterns
2184
- }, results);
2185
- results.sort((a, b) => {
2186
- if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
2187
- return a.relative_path.localeCompare(b.relative_path);
2188
- });
2189
- return {
2190
- success: true,
2191
- pattern,
2192
- directory: resolvedDir,
2193
- results,
2194
- total_found: results.length,
2195
- truncated: results.length >= MAX_RESULTS2
2196
- };
2197
- } catch (error) {
2198
- return {
2199
- success: false,
2200
- pattern: args.pattern || "",
2201
- directory: args.directory || process.cwd(),
2202
- results: [],
2203
- total_found: 0,
2204
- truncated: false,
2205
- error: `Unexpected error: ${error.message}`
2206
- };
2317
+ saveTasksToFile();
2318
+ const stats = calculateStats();
2319
+ const progressBar = generateProgressBar(stats.progress);
2320
+ return createResult(true, `Synced ${taskStore.length} tasks ${progressBar} ${stats.progress}%`);
2321
+ }
2322
+ const fullArgs = args;
2323
+ switch (fullArgs.action) {
2324
+ case "list":
2325
+ return listTasks();
2326
+ case "add":
2327
+ return addTasks(fullArgs.tasks);
2328
+ case "complete":
2329
+ if (!fullArgs.taskId) {
2330
+ return createResult(false, "taskId required for complete action");
2331
+ }
2332
+ return completeTask(fullArgs.taskId);
2333
+ case "update":
2334
+ if (!fullArgs.taskId) {
2335
+ return createResult(false, "taskId required for update action");
2336
+ }
2337
+ return updateTask(fullArgs.taskId, fullArgs.description, fullArgs.priority);
2338
+ case "remove":
2339
+ if (!fullArgs.taskId) {
2340
+ return createResult(false, "taskId required for remove action");
2341
+ }
2342
+ return removeTask(fullArgs.taskId);
2343
+ case "clear":
2344
+ return clearCompleted();
2345
+ case "sync":
2346
+ return addTasks(fullArgs.tasks);
2347
+ default:
2348
+ return createResult(false, `Unknown action: ${fullArgs.action}`);
2207
2349
  }
2208
2350
  }
2209
-
2210
- // src/app/agent/tools/natives/grep_search.ts
2211
- import path7 from "path";
2212
- import { promises as fsPromises2 } from "fs";
2213
- var MAX_RESULTS3 = 100;
2214
- var MAX_FILE_SIZE2 = 1024 * 1024;
2215
- var MAX_LINE_LENGTH = 500;
2216
- var DEFAULT_IGNORE2 = [
2217
- "node_modules",
2218
- ".git",
2219
- ".venv",
2220
- "venv",
2221
- "__pycache__",
2222
- ".cache",
2223
- "dist",
2224
- "build",
2225
- ".next",
2226
- "coverage",
2227
- "*.min.js",
2228
- "*.min.css",
2229
- "*.map",
2230
- "*.lock",
2231
- "package-lock.json",
2232
- "yarn.lock",
2233
- "pnpm-lock.yaml"
2234
- ];
2235
- var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
2236
- ".ts",
2237
- ".tsx",
2238
- ".js",
2239
- ".jsx",
2240
- ".mjs",
2241
- ".cjs",
2242
- ".py",
2243
- ".pyw",
2244
- ".java",
2245
- ".kt",
2246
- ".scala",
2247
- ".go",
2248
- ".rs",
2249
- ".rb",
2250
- ".php",
2251
- ".cs",
2252
- ".cpp",
2253
- ".c",
2254
- ".h",
2255
- ".hpp",
2256
- ".swift",
2257
- ".vue",
2258
- ".svelte",
2259
- ".html",
2260
- ".htm",
2261
- ".xml",
2262
- ".svg",
2263
- ".css",
2264
- ".scss",
2265
- ".sass",
2266
- ".less",
2267
- ".json",
2268
- ".yaml",
2269
- ".yml",
2270
- ".toml",
2271
- ".md",
2272
- ".mdx",
2273
- ".txt",
2274
- ".rst",
2275
- ".sh",
2276
- ".bash",
2277
- ".zsh",
2278
- ".fish",
2279
- ".sql",
2280
- ".graphql",
2281
- ".gql",
2282
- ".env",
2283
- ".env.local",
2284
- ".env.example",
2285
- ".gitignore",
2286
- ".dockerignore",
2287
- ".eslintrc",
2288
- ".prettierrc",
2289
- "Dockerfile",
2290
- "Makefile",
2291
- "Rakefile"
2292
- ]);
2293
- function isTextFile(filepath) {
2294
- const ext = path7.extname(filepath).toLowerCase();
2295
- const basename = path7.basename(filepath);
2296
- if (TEXT_EXTENSIONS.has(ext)) return true;
2297
- if (TEXT_EXTENSIONS.has(basename)) return true;
2298
- if (basename.startsWith(".") && !ext) return true;
2299
- return false;
2300
- }
2301
- function shouldIgnore2(name) {
2302
- for (const pattern of DEFAULT_IGNORE2) {
2303
- if (pattern.includes("*")) {
2304
- const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
2305
- if (regex.test(name)) return true;
2306
- } else if (name === pattern) {
2351
+
2352
+ // src/app/agent/tools/natives/find_by_name.ts
2353
+ import path6 from "path";
2354
+ import { promises as fsPromises } from "fs";
2355
+ var DEFAULT_IGNORE_PATTERNS = [
2356
+ "node_modules",
2357
+ ".git",
2358
+ ".venv",
2359
+ "venv",
2360
+ "__pycache__",
2361
+ ".cache",
2362
+ "dist",
2363
+ "build",
2364
+ ".next",
2365
+ ".nuxt",
2366
+ "coverage",
2367
+ ".nyc_output",
2368
+ ".pytest_cache",
2369
+ "target",
2370
+ // Rust
2371
+ "vendor"
2372
+ // Go, PHP
2373
+ ];
2374
+ var MAX_RESULTS2 = 100;
2375
+ var MAX_DEPTH_DEFAULT = 10;
2376
+ function globToRegex(glob) {
2377
+ let regex = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "{{DOUBLESTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, ".").replace(/{{DOUBLESTAR}}/g, ".*");
2378
+ return new RegExp(`^${regex}$`, "i");
2379
+ }
2380
+ function shouldIgnore(name, ignorePatterns, includeHidden) {
2381
+ if (!includeHidden && name.startsWith(".")) {
2382
+ return true;
2383
+ }
2384
+ for (const pattern of ignorePatterns) {
2385
+ if (name === pattern || name.match(globToRegex(pattern))) {
2307
2386
  return true;
2308
2387
  }
2309
2388
  }
2310
2389
  return false;
2311
2390
  }
2312
- function createSearchPattern(query, isRegex, caseInsensitive) {
2313
- try {
2314
- const flags = caseInsensitive ? "gi" : "g";
2315
- if (isRegex) {
2316
- return new RegExp(query, flags);
2317
- } else {
2318
- const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2319
- return new RegExp(escaped, flags);
2320
- }
2321
- } catch (error) {
2322
- throw new Error(`Invalid regex pattern: ${error.message}`);
2323
- }
2324
- }
2325
- function matchesIncludePattern(filename, includePatterns) {
2326
- if (!includePatterns || includePatterns.length === 0) return true;
2327
- for (const pattern of includePatterns) {
2328
- const regex = new RegExp("^" + pattern.replace(/\*/g, ".*").replace(/\?/g, ".") + "$", "i");
2329
- if (regex.test(filename)) return true;
2330
- }
2331
- return false;
2391
+ function matchesExtensions(filename, extensions) {
2392
+ if (!extensions || extensions.length === 0) return true;
2393
+ const ext = path6.extname(filename).toLowerCase();
2394
+ return extensions.some((e) => {
2395
+ const normalizedExt = e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`;
2396
+ return ext === normalizedExt;
2397
+ });
2332
2398
  }
2333
- async function searchFile(filepath, baseDir, pattern, contextLines, matches, maxResults) {
2334
- let fileMatches = 0;
2335
- try {
2336
- const stats = await fsPromises2.stat(filepath);
2337
- if (stats.size > MAX_FILE_SIZE2) return 0;
2338
- const content = await fsPromises2.readFile(filepath, "utf-8");
2339
- const lines = content.split("\n");
2340
- const relativePath = path7.relative(baseDir, filepath);
2341
- for (let i = 0; i < lines.length && matches.length < maxResults; i++) {
2342
- const line = lines[i];
2343
- pattern.lastIndex = 0;
2344
- let match;
2345
- while ((match = pattern.exec(line)) !== null && matches.length < maxResults) {
2346
- const lineContent = line.length > MAX_LINE_LENGTH ? line.substring(0, MAX_LINE_LENGTH) + "..." : line;
2347
- const searchMatch = {
2348
- file: filepath,
2349
- relative_path: relativePath,
2350
- line_number: i + 1,
2351
- line_content: lineContent.trim(),
2352
- match_start: match.index,
2353
- match_end: match.index + match[0].length
2354
- };
2355
- if (contextLines > 0) {
2356
- searchMatch.context_before = lines.slice(Math.max(0, i - contextLines), i).map((l) => l.length > MAX_LINE_LENGTH ? l.substring(0, MAX_LINE_LENGTH) + "..." : l);
2357
- searchMatch.context_after = lines.slice(i + 1, i + 1 + contextLines).map((l) => l.length > MAX_LINE_LENGTH ? l.substring(0, MAX_LINE_LENGTH) + "..." : l);
2358
- }
2359
- matches.push(searchMatch);
2360
- fileMatches++;
2361
- if (!pattern.global) break;
2362
- }
2363
- }
2364
- } catch (error) {
2399
+ async function searchDirectory(dir, pattern, baseDir, options, results) {
2400
+ if (options.currentDepth > options.maxDepth || results.length >= MAX_RESULTS2) {
2401
+ return;
2365
2402
  }
2366
- return fileMatches;
2367
- }
2368
- async function searchDirectory2(dir, baseDir, pattern, includePatterns, contextLines, matches, maxResults, stats) {
2369
- if (matches.length >= maxResults) return;
2370
2403
  let entries;
2371
2404
  try {
2372
- entries = await fsPromises2.readdir(dir, { withFileTypes: true });
2373
- } catch {
2405
+ entries = await fsPromises.readdir(dir, { withFileTypes: true });
2406
+ } catch (error) {
2374
2407
  return;
2375
2408
  }
2376
2409
  for (const entry of entries) {
2377
- if (matches.length >= maxResults) break;
2410
+ if (results.length >= MAX_RESULTS2) break;
2378
2411
  const name = entry.name;
2379
- if (shouldIgnore2(name)) continue;
2380
- const fullPath = path7.join(dir, name);
2412
+ if (shouldIgnore(name, options.ignorePatterns, options.includeHidden)) {
2413
+ continue;
2414
+ }
2415
+ const fullPath = path6.join(dir, name);
2416
+ const relativePath = path6.relative(baseDir, fullPath);
2381
2417
  if (entry.isDirectory()) {
2382
- await searchDirectory2(fullPath, baseDir, pattern, includePatterns, contextLines, matches, maxResults, stats);
2418
+ if (pattern.test(name)) {
2419
+ results.push({
2420
+ path: fullPath,
2421
+ relative_path: relativePath,
2422
+ type: "directory"
2423
+ });
2424
+ }
2425
+ await searchDirectory(fullPath, pattern, baseDir, {
2426
+ ...options,
2427
+ currentDepth: options.currentDepth + 1
2428
+ }, results);
2383
2429
  } else if (entry.isFile()) {
2384
- if (!isTextFile(name)) continue;
2385
- if (!matchesIncludePattern(name, includePatterns)) continue;
2386
- stats.filesSearched++;
2387
- const found = await searchFile(fullPath, baseDir, pattern, contextLines, matches, maxResults);
2388
- if (found > 0) stats.filesWithMatches++;
2430
+ if (pattern.test(name) && matchesExtensions(name, options.extensions)) {
2431
+ try {
2432
+ const stats = await fsPromises.stat(fullPath);
2433
+ results.push({
2434
+ path: fullPath,
2435
+ relative_path: relativePath,
2436
+ type: "file",
2437
+ size: stats.size,
2438
+ modified: stats.mtime.toISOString()
2439
+ });
2440
+ } catch {
2441
+ results.push({
2442
+ path: fullPath,
2443
+ relative_path: relativePath,
2444
+ type: "file"
2445
+ });
2446
+ }
2447
+ }
2389
2448
  }
2390
2449
  }
2391
2450
  }
2392
- async function grepSearch(args) {
2451
+ async function findByName(args) {
2393
2452
  try {
2394
2453
  const {
2395
- query,
2396
- path: searchPath,
2397
- case_insensitive = true,
2398
- is_regex = false,
2399
- include_patterns,
2400
- max_results = MAX_RESULTS3,
2401
- context_lines = 0
2454
+ pattern,
2455
+ directory = process.cwd(),
2456
+ extensions,
2457
+ max_depth = MAX_DEPTH_DEFAULT,
2458
+ include_hidden = false,
2459
+ exclude_patterns = []
2402
2460
  } = args;
2403
- if (!query || typeof query !== "string") {
2404
- return {
2405
- success: false,
2406
- query: query || "",
2407
- search_path: searchPath || "",
2408
- matches: [],
2409
- files_searched: 0,
2410
- files_with_matches: 0,
2411
- total_matches: 0,
2412
- truncated: false,
2413
- error: "Query is required and must be a string"
2414
- };
2415
- }
2416
- if (!searchPath) {
2417
- return {
2418
- success: false,
2419
- query,
2420
- search_path: "",
2421
- matches: [],
2422
- files_searched: 0,
2423
- files_with_matches: 0,
2424
- total_matches: 0,
2425
- truncated: false,
2426
- error: "Search path is required"
2427
- };
2428
- }
2429
- const resolvedPath = path7.resolve(searchPath);
2430
- let stats;
2431
- try {
2432
- stats = await fsPromises2.stat(resolvedPath);
2433
- } catch {
2461
+ if (!pattern || typeof pattern !== "string") {
2434
2462
  return {
2435
2463
  success: false,
2436
- query,
2437
- search_path: resolvedPath,
2438
- matches: [],
2439
- files_searched: 0,
2440
- files_with_matches: 0,
2441
- total_matches: 0,
2464
+ pattern: pattern || "",
2465
+ directory,
2466
+ results: [],
2467
+ total_found: 0,
2442
2468
  truncated: false,
2443
- error: `Path not found: ${resolvedPath}`
2469
+ error: "Pattern is required and must be a string"
2444
2470
  };
2445
2471
  }
2446
- let pattern;
2472
+ const resolvedDir = path6.resolve(directory);
2447
2473
  try {
2448
- pattern = createSearchPattern(query, is_regex, case_insensitive);
2474
+ const stats = await fsPromises.stat(resolvedDir);
2475
+ if (!stats.isDirectory()) {
2476
+ return {
2477
+ success: false,
2478
+ pattern,
2479
+ directory: resolvedDir,
2480
+ results: [],
2481
+ total_found: 0,
2482
+ truncated: false,
2483
+ error: `Path is not a directory: ${resolvedDir}`
2484
+ };
2485
+ }
2449
2486
  } catch (error) {
2450
2487
  return {
2451
2488
  success: false,
2452
- query,
2453
- search_path: resolvedPath,
2454
- matches: [],
2455
- files_searched: 0,
2456
- files_with_matches: 0,
2457
- total_matches: 0,
2458
- truncated: false,
2459
- error: error.message
2460
- };
2461
- }
2462
- const matches = [];
2463
- const searchStats = { filesSearched: 0, filesWithMatches: 0 };
2464
- if (stats.isDirectory()) {
2465
- await searchDirectory2(
2466
- resolvedPath,
2467
- resolvedPath,
2468
2489
  pattern,
2469
- include_patterns,
2470
- context_lines,
2471
- matches,
2472
- max_results,
2473
- searchStats
2474
- );
2475
- } else if (stats.isFile()) {
2476
- searchStats.filesSearched = 1;
2477
- const found = await searchFile(resolvedPath, path7.dirname(resolvedPath), pattern, context_lines, matches, max_results);
2478
- if (found > 0) searchStats.filesWithMatches = 1;
2490
+ directory: resolvedDir,
2491
+ results: [],
2492
+ total_found: 0,
2493
+ truncated: false,
2494
+ error: `Directory not found: ${resolvedDir}`
2495
+ };
2479
2496
  }
2497
+ const ignorePatterns = [...DEFAULT_IGNORE_PATTERNS, ...exclude_patterns];
2498
+ const patternRegex = globToRegex(pattern);
2499
+ const results = [];
2500
+ await searchDirectory(resolvedDir, patternRegex, resolvedDir, {
2501
+ extensions,
2502
+ maxDepth: max_depth,
2503
+ currentDepth: 0,
2504
+ includeHidden: include_hidden,
2505
+ ignorePatterns
2506
+ }, results);
2507
+ results.sort((a, b) => {
2508
+ if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
2509
+ return a.relative_path.localeCompare(b.relative_path);
2510
+ });
2480
2511
  return {
2481
2512
  success: true,
2482
- query,
2483
- search_path: resolvedPath,
2484
- matches,
2485
- files_searched: searchStats.filesSearched,
2486
- files_with_matches: searchStats.filesWithMatches,
2487
- total_matches: matches.length,
2488
- truncated: matches.length >= max_results
2513
+ pattern,
2514
+ directory: resolvedDir,
2515
+ results,
2516
+ total_found: results.length,
2517
+ truncated: results.length >= MAX_RESULTS2
2489
2518
  };
2490
2519
  } catch (error) {
2491
2520
  return {
2492
2521
  success: false,
2493
- query: args.query || "",
2494
- search_path: args.path || "",
2495
- matches: [],
2496
- files_searched: 0,
2497
- files_with_matches: 0,
2498
- total_matches: 0,
2522
+ pattern: args.pattern || "",
2523
+ directory: args.directory || process.cwd(),
2524
+ results: [],
2525
+ total_found: 0,
2499
2526
  truncated: false,
2500
2527
  error: `Unexpected error: ${error.message}`
2501
2528
  };
2502
2529
  }
2503
2530
  }
2504
2531
 
2505
- // src/app/agent/tools/natives/view_file_outline.ts
2506
- import path8 from "path";
2507
- import { promises as fsPromises3 } from "fs";
2508
- var LANGUAGE_MAP = {
2509
- ".ts": "typescript",
2510
- ".tsx": "typescript",
2511
- ".js": "javascript",
2512
- ".jsx": "javascript",
2513
- ".mjs": "javascript",
2514
- ".cjs": "javascript",
2515
- ".py": "python",
2516
- ".java": "java",
2517
- ".go": "go",
2518
- ".rs": "rust",
2519
- ".rb": "ruby",
2520
- ".php": "php",
2521
- ".cs": "csharp",
2522
- ".cpp": "cpp",
2523
- ".c": "c",
2524
- ".h": "c",
2525
- ".hpp": "cpp",
2526
- ".swift": "swift",
2527
- ".kt": "kotlin",
2528
- ".scala": "scala"
2529
- };
2530
- var PATTERNS = {
2531
- typescript: [
2532
- // Classes
2533
- /^(?:export\s+)?(?:abstract\s+)?class\s+(\w+)(?:\s+extends\s+\w+)?(?:\s+implements\s+[\w,\s]+)?/,
2534
- // Interfaces
2535
- /^(?:export\s+)?interface\s+(\w+)/,
2536
- // Types
2537
- /^(?:export\s+)?type\s+(\w+)/,
2538
- // Functions (standalone)
2539
- /^(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/,
2540
- // Arrow functions assigned to const/let
2541
- /^(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[^=])\s*=>/,
2542
- // Methods inside classes (indented)
2543
- /^\s+(?:public|private|protected|static|async|readonly|\s)*(\w+)\s*\([^)]*\)\s*(?::\s*[\w<>\[\]|&\s]+)?\s*\{/,
2544
- // Const exports
2545
- /^(?:export\s+)?const\s+(\w+)\s*[:=]/
2546
- ],
2547
- javascript: [
2548
- /^(?:export\s+)?(?:default\s+)?class\s+(\w+)/,
2549
- /^(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/,
2550
- /^(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[^=])\s*=>/,
2551
- /^\s+(?:async\s+)?(\w+)\s*\([^)]*\)\s*\{/,
2552
- /^(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=/
2553
- ],
2554
- python: [
2555
- /^class\s+(\w+)(?:\([^)]*\))?:/,
2556
- /^(?:async\s+)?def\s+(\w+)\s*\(/,
2557
- /^(\w+)\s*=\s*(?:lambda|def)/
2558
- ],
2559
- java: [
2560
- /^(?:public|private|protected)?\s*(?:static)?\s*(?:final)?\s*(?:abstract)?\s*class\s+(\w+)/,
2561
- /^(?:public|private|protected)?\s*(?:static)?\s*(?:final)?\s*interface\s+(\w+)/,
2562
- /^\s*(?:public|private|protected)?\s*(?:static)?\s*(?:final)?\s*(?:synchronized)?\s*(?:\w+(?:<[^>]+>)?)\s+(\w+)\s*\(/
2563
- ],
2564
- go: [
2565
- /^type\s+(\w+)\s+struct\s*\{/,
2566
- /^type\s+(\w+)\s+interface\s*\{/,
2567
- /^func\s+(?:\([^)]+\)\s+)?(\w+)\s*\(/
2568
- ],
2569
- rust: [
2570
- /^(?:pub\s+)?struct\s+(\w+)/,
2571
- /^(?:pub\s+)?enum\s+(\w+)/,
2572
- /^(?:pub\s+)?trait\s+(\w+)/,
2573
- /^(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/,
2574
- /^impl(?:<[^>]+>)?\s+(?:(\w+)|for\s+(\w+))/
2575
- ],
2576
- ruby: [
2577
- /^class\s+(\w+)/,
2578
- /^module\s+(\w+)/,
2579
- /^def\s+(\w+)/
2580
- ],
2581
- php: [
2582
- /^(?:abstract\s+)?class\s+(\w+)/,
2583
- /^interface\s+(\w+)/,
2584
- /^(?:public|private|protected)?\s*(?:static)?\s*function\s+(\w+)/
2585
- ],
2586
- csharp: [
2587
- /^(?:public|private|protected|internal)?\s*(?:static)?\s*(?:partial)?\s*class\s+(\w+)/,
2588
- /^(?:public|private|protected|internal)?\s*interface\s+(\w+)/,
2589
- /^\s*(?:public|private|protected|internal)?\s*(?:static)?\s*(?:async)?\s*(?:\w+(?:<[^>]+>)?)\s+(\w+)\s*\(/
2590
- ],
2591
- swift: [
2592
- /^(?:public|private|internal|open)?\s*class\s+(\w+)/,
2593
- /^(?:public|private|internal)?\s*struct\s+(\w+)/,
2594
- /^(?:public|private|internal)?\s*protocol\s+(\w+)/,
2595
- /^(?:public|private|internal)?\s*(?:static)?\s*func\s+(\w+)/
2596
- ],
2597
- kotlin: [
2598
- /^(?:open|abstract|sealed)?\s*class\s+(\w+)/,
2599
- /^interface\s+(\w+)/,
2600
- /^(?:private|public|internal)?\s*fun\s+(\w+)/
2601
- ],
2602
- scala: [
2603
- /^(?:sealed\s+)?(?:abstract\s+)?class\s+(\w+)/,
2604
- /^object\s+(\w+)/,
2605
- /^trait\s+(\w+)/,
2606
- /^def\s+(\w+)/
2607
- ],
2608
- c: [
2609
- /^(?:static\s+)?(?:inline\s+)?(?:const\s+)?(?:\w+(?:\s*\*)*)\s+(\w+)\s*\([^)]*\)\s*\{/,
2610
- /^typedef\s+struct\s*\w*\s*\{[^}]*\}\s*(\w+)/,
2611
- /^struct\s+(\w+)\s*\{/
2612
- ],
2613
- cpp: [
2614
- /^class\s+(\w+)/,
2615
- /^(?:virtual\s+)?(?:\w+(?:\s*[*&])*)\s+(\w+)\s*\([^)]*\)\s*(?:const)?\s*(?:override)?\s*\{/,
2616
- /^namespace\s+(\w+)\s*\{/
2617
- ]
2618
- };
2619
- function detectLanguage(filepath) {
2620
- const ext = path8.extname(filepath).toLowerCase();
2621
- return LANGUAGE_MAP[ext] || "unknown";
2532
+ // src/app/agent/tools/natives/grep_search.ts
2533
+ import path7 from "path";
2534
+ import { promises as fsPromises2 } from "fs";
2535
+ var MAX_RESULTS3 = 100;
2536
+ var MAX_FILE_SIZE2 = 1024 * 1024;
2537
+ var MAX_LINE_LENGTH = 500;
2538
+ var DEFAULT_IGNORE2 = [
2539
+ "node_modules",
2540
+ ".git",
2541
+ ".venv",
2542
+ "venv",
2543
+ "__pycache__",
2544
+ ".cache",
2545
+ "dist",
2546
+ "build",
2547
+ ".next",
2548
+ "coverage",
2549
+ "*.min.js",
2550
+ "*.min.css",
2551
+ "*.map",
2552
+ "*.lock",
2553
+ "package-lock.json",
2554
+ "yarn.lock",
2555
+ "pnpm-lock.yaml"
2556
+ ];
2557
+ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
2558
+ ".ts",
2559
+ ".tsx",
2560
+ ".js",
2561
+ ".jsx",
2562
+ ".mjs",
2563
+ ".cjs",
2564
+ ".py",
2565
+ ".pyw",
2566
+ ".java",
2567
+ ".kt",
2568
+ ".scala",
2569
+ ".go",
2570
+ ".rs",
2571
+ ".rb",
2572
+ ".php",
2573
+ ".cs",
2574
+ ".cpp",
2575
+ ".c",
2576
+ ".h",
2577
+ ".hpp",
2578
+ ".swift",
2579
+ ".vue",
2580
+ ".svelte",
2581
+ ".html",
2582
+ ".htm",
2583
+ ".xml",
2584
+ ".svg",
2585
+ ".css",
2586
+ ".scss",
2587
+ ".sass",
2588
+ ".less",
2589
+ ".json",
2590
+ ".yaml",
2591
+ ".yml",
2592
+ ".toml",
2593
+ ".md",
2594
+ ".mdx",
2595
+ ".txt",
2596
+ ".rst",
2597
+ ".sh",
2598
+ ".bash",
2599
+ ".zsh",
2600
+ ".fish",
2601
+ ".sql",
2602
+ ".graphql",
2603
+ ".gql",
2604
+ ".env",
2605
+ ".env.local",
2606
+ ".env.example",
2607
+ ".gitignore",
2608
+ ".dockerignore",
2609
+ ".eslintrc",
2610
+ ".prettierrc",
2611
+ "Dockerfile",
2612
+ "Makefile",
2613
+ "Rakefile"
2614
+ ]);
2615
+ function isTextFile(filepath) {
2616
+ const ext = path7.extname(filepath).toLowerCase();
2617
+ const basename = path7.basename(filepath);
2618
+ if (TEXT_EXTENSIONS.has(ext)) return true;
2619
+ if (TEXT_EXTENSIONS.has(basename)) return true;
2620
+ if (basename.startsWith(".") && !ext) return true;
2621
+ return false;
2622
2622
  }
2623
- function determineItemType(line, language) {
2624
- const normalizedLine = line.trim().toLowerCase();
2625
- if (normalizedLine.includes("class ")) return "class";
2626
- if (normalizedLine.includes("interface ")) return "interface";
2627
- if (normalizedLine.includes("type ")) return "type";
2628
- if (normalizedLine.startsWith("import ")) return "import";
2629
- if (normalizedLine.includes("export ") && !normalizedLine.includes("function") && !normalizedLine.includes("class")) return "export";
2630
- if (line.startsWith(" ") || line.startsWith(" ")) {
2631
- if (normalizedLine.includes("function") || normalizedLine.includes("def ") || normalizedLine.includes("fn ") || normalizedLine.includes("func ")) {
2632
- return "method";
2623
+ function shouldIgnore2(name) {
2624
+ for (const pattern of DEFAULT_IGNORE2) {
2625
+ if (pattern.includes("*")) {
2626
+ const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
2627
+ if (regex.test(name)) return true;
2628
+ } else if (name === pattern) {
2629
+ return true;
2633
2630
  }
2634
- if (/^\s+\w+\s*\(/.test(line)) return "method";
2635
2631
  }
2636
- if (normalizedLine.includes("function ") || normalizedLine.includes("def ") || normalizedLine.includes("fn ") || normalizedLine.includes("func ")) {
2637
- return "function";
2632
+ return false;
2633
+ }
2634
+ function createSearchPattern(query, isRegex, caseInsensitive) {
2635
+ try {
2636
+ const flags = caseInsensitive ? "gi" : "g";
2637
+ if (isRegex) {
2638
+ return new RegExp(query, flags);
2639
+ } else {
2640
+ const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2641
+ return new RegExp(escaped, flags);
2642
+ }
2643
+ } catch (error) {
2644
+ throw new Error(`Invalid regex pattern: ${error.message}`);
2638
2645
  }
2639
- if (normalizedLine.includes("const ") || normalizedLine.includes("let ") || normalizedLine.includes("var ")) {
2640
- return normalizedLine.includes("=>") ? "function" : "const";
2646
+ }
2647
+ function matchesIncludePattern(filename, includePatterns) {
2648
+ if (!includePatterns || includePatterns.length === 0) return true;
2649
+ for (const pattern of includePatterns) {
2650
+ const regex = new RegExp("^" + pattern.replace(/\*/g, ".*").replace(/\?/g, ".") + "$", "i");
2651
+ if (regex.test(filename)) return true;
2641
2652
  }
2642
- return "function";
2653
+ return false;
2643
2654
  }
2644
- async function extractOutline(filepath, content, language) {
2645
- const items = [];
2646
- const lines = content.split("\n");
2647
- const patterns = PATTERNS[language] || PATTERNS.javascript;
2648
- let currentClass = null;
2649
- let braceCount = 0;
2650
- for (let i = 0; i < lines.length; i++) {
2651
- const line = lines[i];
2652
- const lineNumber = i + 1;
2653
- const openBraces = (line.match(/\{/g) || []).length;
2654
- const closeBraces = (line.match(/\}/g) || []).length;
2655
- braceCount += openBraces - closeBraces;
2656
- if (braceCount <= 0 && currentClass) {
2657
- currentClass = null;
2658
- }
2659
- for (const pattern of patterns) {
2660
- const match = line.match(pattern);
2661
- if (match) {
2662
- const name = match[1] || match[2];
2663
- if (!name) continue;
2664
- const itemType = determineItemType(line, language);
2665
- if (!itemType) continue;
2666
- const exported = line.includes("export ");
2667
- const isInsideClass = currentClass && (line.startsWith(" ") || line.startsWith(" "));
2668
- const item = {
2669
- type: itemType,
2670
- name,
2671
- line_start: lineNumber,
2672
- signature: line.trim().substring(0, 120),
2673
- exported
2655
+ async function searchFile(filepath, baseDir, pattern, contextLines, matches, maxResults) {
2656
+ let fileMatches = 0;
2657
+ try {
2658
+ const stats = await fsPromises2.stat(filepath);
2659
+ if (stats.size > MAX_FILE_SIZE2) return 0;
2660
+ const content = await fsPromises2.readFile(filepath, "utf-8");
2661
+ const lines = content.split("\n");
2662
+ const relativePath = path7.relative(baseDir, filepath);
2663
+ for (let i = 0; i < lines.length && matches.length < maxResults; i++) {
2664
+ const line = lines[i];
2665
+ pattern.lastIndex = 0;
2666
+ let match;
2667
+ while ((match = pattern.exec(line)) !== null && matches.length < maxResults) {
2668
+ const lineContent = line.length > MAX_LINE_LENGTH ? line.substring(0, MAX_LINE_LENGTH) + "..." : line;
2669
+ const searchMatch = {
2670
+ file: filepath,
2671
+ relative_path: relativePath,
2672
+ line_number: i + 1,
2673
+ line_content: lineContent.trim(),
2674
+ match_start: match.index,
2675
+ match_end: match.index + match[0].length
2674
2676
  };
2675
- if (isInsideClass && itemType === "method" && currentClass) {
2676
- item.parent = currentClass;
2677
- }
2678
- if (itemType === "class") {
2679
- currentClass = name;
2677
+ if (contextLines > 0) {
2678
+ searchMatch.context_before = lines.slice(Math.max(0, i - contextLines), i).map((l) => l.length > MAX_LINE_LENGTH ? l.substring(0, MAX_LINE_LENGTH) + "..." : l);
2679
+ searchMatch.context_after = lines.slice(i + 1, i + 1 + contextLines).map((l) => l.length > MAX_LINE_LENGTH ? l.substring(0, MAX_LINE_LENGTH) + "..." : l);
2680
2680
  }
2681
- items.push(item);
2682
- break;
2681
+ matches.push(searchMatch);
2682
+ fileMatches++;
2683
+ if (!pattern.global) break;
2683
2684
  }
2684
2685
  }
2686
+ } catch (error) {
2685
2687
  }
2686
- return items;
2688
+ return fileMatches;
2687
2689
  }
2688
- function generateSummary(items, totalLines) {
2689
- const counts = {};
2690
- for (const item of items) {
2691
- counts[item.type] = (counts[item.type] || 0) + 1;
2690
+ async function searchDirectory2(dir, baseDir, pattern, includePatterns, contextLines, matches, maxResults, stats) {
2691
+ if (matches.length >= maxResults) return;
2692
+ let entries;
2693
+ try {
2694
+ entries = await fsPromises2.readdir(dir, { withFileTypes: true });
2695
+ } catch {
2696
+ return;
2697
+ }
2698
+ for (const entry of entries) {
2699
+ if (matches.length >= maxResults) break;
2700
+ const name = entry.name;
2701
+ if (shouldIgnore2(name)) continue;
2702
+ const fullPath = path7.join(dir, name);
2703
+ if (entry.isDirectory()) {
2704
+ await searchDirectory2(fullPath, baseDir, pattern, includePatterns, contextLines, matches, maxResults, stats);
2705
+ } else if (entry.isFile()) {
2706
+ if (!isTextFile(name)) continue;
2707
+ if (!matchesIncludePattern(name, includePatterns)) continue;
2708
+ stats.filesSearched++;
2709
+ const found = await searchFile(fullPath, baseDir, pattern, contextLines, matches, maxResults);
2710
+ if (found > 0) stats.filesWithMatches++;
2711
+ }
2692
2712
  }
2693
- const parts = [`${totalLines} lines`];
2694
- if (counts.class) parts.push(`${counts.class} class${counts.class > 1 ? "es" : ""}`);
2695
- if (counts.interface) parts.push(`${counts.interface} interface${counts.interface > 1 ? "s" : ""}`);
2696
- if (counts.function) parts.push(`${counts.function} function${counts.function > 1 ? "s" : ""}`);
2697
- if (counts.method) parts.push(`${counts.method} method${counts.method > 1 ? "s" : ""}`);
2698
- if (counts.type) parts.push(`${counts.type} type${counts.type > 1 ? "s" : ""}`);
2699
- if (counts.const) parts.push(`${counts.const} const${counts.const > 1 ? "s" : ""}`);
2700
- return parts.join(", ");
2701
2713
  }
2702
- async function viewFileOutline(args) {
2714
+ async function grepSearch(args) {
2703
2715
  try {
2704
- const { file_path } = args;
2705
- if (!file_path || typeof file_path !== "string") {
2716
+ const {
2717
+ query,
2718
+ path: searchPath,
2719
+ case_insensitive = true,
2720
+ is_regex = false,
2721
+ include_patterns,
2722
+ max_results = MAX_RESULTS3,
2723
+ context_lines = 0
2724
+ } = args;
2725
+ if (!query || typeof query !== "string") {
2706
2726
  return {
2707
2727
  success: false,
2708
- file_path: file_path || "",
2709
- language: "unknown",
2710
- total_lines: 0,
2711
- items: [],
2712
- summary: "",
2713
- error: "file_path is required and must be a string"
2728
+ query: query || "",
2729
+ search_path: searchPath || "",
2730
+ matches: [],
2731
+ files_searched: 0,
2732
+ files_with_matches: 0,
2733
+ total_matches: 0,
2734
+ truncated: false,
2735
+ error: "Query is required and must be a string"
2714
2736
  };
2715
2737
  }
2716
- const resolvedPath = path8.resolve(file_path);
2717
- let content;
2718
- try {
2719
- content = await fsPromises3.readFile(resolvedPath, "utf-8");
2720
- } catch (error) {
2738
+ if (!searchPath) {
2721
2739
  return {
2722
2740
  success: false,
2723
- file_path: resolvedPath,
2724
- language: "unknown",
2725
- total_lines: 0,
2726
- items: [],
2727
- summary: "",
2728
- error: error.code === "ENOENT" ? `File not found: ${resolvedPath}` : `Error reading file: ${error.message}`
2741
+ query,
2742
+ search_path: "",
2743
+ matches: [],
2744
+ files_searched: 0,
2745
+ files_with_matches: 0,
2746
+ total_matches: 0,
2747
+ truncated: false,
2748
+ error: "Search path is required"
2729
2749
  };
2730
2750
  }
2731
- const language = detectLanguage(resolvedPath);
2732
- const lines = content.split("\n");
2733
- const totalLines = lines.length;
2734
- const items = await extractOutline(resolvedPath, content, language);
2735
- return {
2736
- success: true,
2737
- file_path: resolvedPath,
2738
- language,
2739
- total_lines: totalLines,
2740
- items,
2741
- summary: generateSummary(items, totalLines)
2742
- };
2743
- } catch (error) {
2744
- return {
2745
- success: false,
2746
- file_path: args.file_path || "",
2747
- language: "unknown",
2748
- total_lines: 0,
2749
- items: [],
2750
- summary: "",
2751
- error: `Unexpected error: ${error.message}`
2752
- };
2753
- }
2754
- }
2755
-
2756
- // src/app/agent/tools/natives/async_command.ts
2757
- import os2 from "os";
2758
- import { spawn } from "child_process";
2759
- import { v4 as uuidv42 } from "uuid";
2760
- var runningCommands = /* @__PURE__ */ new Map();
2761
- var MAX_OUTPUT_SIZE = 3e4;
2762
- var MAX_STORED_COMMANDS = 50;
2763
- var OUTPUT_TRUNCATION_MSG = "\n[OUTPUT TRUNCATED - 30KB/200 lines max]";
2764
- var DANGEROUS_PATTERNS = [
2765
- // Elevação de privilégios
2766
- /^sudo\s+/i,
2767
- /^doas\s+/i,
2768
- /^su\s+/i,
2769
- /^pkexec\s+/i,
2770
- /\|\s*sudo\s+/i,
2771
- /;\s*sudo\s+/i,
2772
- /&&\s*sudo\s+/i,
2773
- // Comandos destrutivos
2774
- /\brm\s+(-[rf]+\s+)*[\/~]/i,
2775
- /\brm\s+-[rf]*\s+\*/i,
2776
- /\bchmod\s+(777|666)\s+\//i,
2777
- /\bdd\s+.*of=\/dev\/(sd|hd|nvme)/i,
2778
- /\bmkfs\./i,
2779
- // Fork bombs
2780
- /:\(\)\s*\{\s*:\|:&\s*\}\s*;:/,
2781
- // Remote code exec
2782
- /\bcurl\s+.*\|\s*(ba)?sh/i,
2783
- /\bwget\s+.*\|\s*(ba)?sh/i
2784
- ];
2785
- var INTERACTIVE_PATTERNS = [
2786
- /^(vim|vi|nano|emacs|pico)\s*/i,
2787
- /^(less|more|most)\s*/i,
2788
- /^(top|htop|btop|atop|nmon)\s*/i,
2789
- /^(ssh|telnet|ftp|sftp)\s+/i,
2790
- /^(mysql|psql|redis-cli|mongo|mongosh)\s*$/i,
2791
- /^(python|python3|node|ruby|irb|lua)\s*$/i,
2792
- /^(gdb|lldb)\s*/i
2793
- ];
2794
- function cleanupOldCommands() {
2795
- if (runningCommands.size <= MAX_STORED_COMMANDS) return;
2796
- const commands = Array.from(runningCommands.entries()).filter(([_, cmd]) => cmd.status !== "running").sort((a, b) => (a[1].endTime || 0) - (b[1].endTime || 0));
2797
- const toRemove = commands.slice(0, commands.length - MAX_STORED_COMMANDS + 10);
2798
- for (const [id] of toRemove) {
2799
- runningCommands.delete(id);
2800
- }
2801
- }
2802
- function isDangerousCommand(command) {
2803
- const trimmed = command.trim();
2804
- for (const pattern of DANGEROUS_PATTERNS) {
2805
- if (pattern.test(trimmed)) {
2806
- return "Command requires elevated privileges (sudo/doas) which cannot be handled in async mode";
2807
- }
2808
- }
2809
- for (const pattern of INTERACTIVE_PATTERNS) {
2810
- if (pattern.test(trimmed)) {
2811
- return "Interactive commands are not supported in async mode";
2812
- }
2813
- }
2814
- return null;
2815
- }
2816
- async function runCommandAsync(args) {
2817
- try {
2818
- const {
2819
- command,
2820
- cwd = process.cwd(),
2821
- timeout = 0
2822
- } = args;
2823
- if (!command || typeof command !== "string") {
2751
+ const resolvedPath = path7.resolve(searchPath);
2752
+ let stats;
2753
+ try {
2754
+ stats = await fsPromises2.stat(resolvedPath);
2755
+ } catch {
2824
2756
  return {
2825
2757
  success: false,
2826
- error: "command is required and must be a string"
2758
+ query,
2759
+ search_path: resolvedPath,
2760
+ matches: [],
2761
+ files_searched: 0,
2762
+ files_with_matches: 0,
2763
+ total_matches: 0,
2764
+ truncated: false,
2765
+ error: `Path not found: ${resolvedPath}`
2827
2766
  };
2828
2767
  }
2829
- const dangerReason = isDangerousCommand(command);
2830
- if (dangerReason) {
2768
+ let pattern;
2769
+ try {
2770
+ pattern = createSearchPattern(query, is_regex, case_insensitive);
2771
+ } catch (error) {
2831
2772
  return {
2832
2773
  success: false,
2833
- error: dangerReason
2774
+ query,
2775
+ search_path: resolvedPath,
2776
+ matches: [],
2777
+ files_searched: 0,
2778
+ files_with_matches: 0,
2779
+ total_matches: 0,
2780
+ truncated: false,
2781
+ error: error.message
2834
2782
  };
2835
2783
  }
2836
- const commandId = uuidv42().substring(0, 8);
2837
- const platform = os2.platform();
2838
- let shellCmd;
2839
- let shellArgs;
2840
- if (platform === "win32") {
2841
- shellCmd = process.env.COMSPEC || "cmd.exe";
2842
- shellArgs = ["/c", command];
2843
- } else {
2844
- shellCmd = process.env.SHELL || "/bin/bash";
2845
- shellArgs = ["-c", command];
2784
+ const matches = [];
2785
+ const searchStats = { filesSearched: 0, filesWithMatches: 0 };
2786
+ if (stats.isDirectory()) {
2787
+ await searchDirectory2(
2788
+ resolvedPath,
2789
+ resolvedPath,
2790
+ pattern,
2791
+ include_patterns,
2792
+ context_lines,
2793
+ matches,
2794
+ max_results,
2795
+ searchStats
2796
+ );
2797
+ } else if (stats.isFile()) {
2798
+ searchStats.filesSearched = 1;
2799
+ const found = await searchFile(resolvedPath, path7.dirname(resolvedPath), pattern, context_lines, matches, max_results);
2800
+ if (found > 0) searchStats.filesWithMatches = 1;
2846
2801
  }
2847
- const entry = {
2848
- id: commandId,
2849
- command,
2850
- status: "running",
2851
- stdout: "",
2852
- stderr: "",
2853
- exitCode: null,
2854
- startTime: Date.now(),
2855
- process: null
2802
+ return {
2803
+ success: true,
2804
+ query,
2805
+ search_path: resolvedPath,
2806
+ matches,
2807
+ files_searched: searchStats.filesSearched,
2808
+ files_with_matches: searchStats.filesWithMatches,
2809
+ total_matches: matches.length,
2810
+ truncated: matches.length >= max_results
2856
2811
  };
2857
- const child = spawn(shellCmd, shellArgs, {
2858
- cwd,
2859
- env: process.env,
2860
- windowsHide: true
2861
- });
2862
- entry.process = child;
2863
- let stdoutTruncated = false;
2864
- child.stdout?.on("data", (data) => {
2865
- if (!stdoutTruncated) {
2866
- entry.stdout += data.toString();
2867
- if (entry.stdout.length > MAX_OUTPUT_SIZE) {
2868
- entry.stdout = entry.stdout.substring(0, MAX_OUTPUT_SIZE) + OUTPUT_TRUNCATION_MSG;
2869
- stdoutTruncated = true;
2870
- }
2871
- }
2872
- });
2873
- let stderrTruncated = false;
2874
- child.stderr?.on("data", (data) => {
2875
- if (!stderrTruncated) {
2876
- entry.stderr += data.toString();
2877
- if (entry.stderr.length > MAX_OUTPUT_SIZE) {
2878
- entry.stderr = entry.stderr.substring(0, MAX_OUTPUT_SIZE) + OUTPUT_TRUNCATION_MSG;
2879
- stderrTruncated = true;
2880
- }
2881
- }
2882
- });
2883
- child.on("close", (code) => {
2884
- entry.exitCode = code;
2885
- entry.status = code === 0 ? "completed" : "error";
2886
- entry.endTime = Date.now();
2887
- entry.process = null;
2888
- });
2889
- child.on("error", (error) => {
2890
- entry.stderr += `
2891
- Process error: ${error.message}`;
2892
- entry.status = "error";
2893
- entry.endTime = Date.now();
2894
- entry.process = null;
2895
- });
2896
- if (timeout > 0) {
2897
- setTimeout(() => {
2898
- if (entry.status === "running") {
2899
- child.kill("SIGTERM");
2900
- setTimeout(() => {
2901
- if (entry.status === "running") {
2902
- child.kill("SIGKILL");
2903
- }
2904
- }, 2e3);
2905
- entry.status = "timeout";
2906
- entry.stderr += `
2907
- Command timed out after ${timeout} seconds`;
2908
- }
2909
- }, timeout * 1e3);
2812
+ } catch (error) {
2813
+ return {
2814
+ success: false,
2815
+ query: args.query || "",
2816
+ search_path: args.path || "",
2817
+ matches: [],
2818
+ files_searched: 0,
2819
+ files_with_matches: 0,
2820
+ total_matches: 0,
2821
+ truncated: false,
2822
+ error: `Unexpected error: ${error.message}`
2823
+ };
2824
+ }
2825
+ }
2826
+
2827
+ // src/app/agent/tools/natives/view_file_outline.ts
2828
+ import path8 from "path";
2829
+ import { promises as fsPromises3 } from "fs";
2830
+ var LANGUAGE_MAP = {
2831
+ ".ts": "typescript",
2832
+ ".tsx": "typescript",
2833
+ ".js": "javascript",
2834
+ ".jsx": "javascript",
2835
+ ".mjs": "javascript",
2836
+ ".cjs": "javascript",
2837
+ ".py": "python",
2838
+ ".java": "java",
2839
+ ".go": "go",
2840
+ ".rs": "rust",
2841
+ ".rb": "ruby",
2842
+ ".php": "php",
2843
+ ".cs": "csharp",
2844
+ ".cpp": "cpp",
2845
+ ".c": "c",
2846
+ ".h": "c",
2847
+ ".hpp": "cpp",
2848
+ ".swift": "swift",
2849
+ ".kt": "kotlin",
2850
+ ".scala": "scala"
2851
+ };
2852
+ var PATTERNS = {
2853
+ typescript: [
2854
+ // Classes
2855
+ /^(?:export\s+)?(?:abstract\s+)?class\s+(\w+)(?:\s+extends\s+\w+)?(?:\s+implements\s+[\w,\s]+)?/,
2856
+ // Interfaces
2857
+ /^(?:export\s+)?interface\s+(\w+)/,
2858
+ // Types
2859
+ /^(?:export\s+)?type\s+(\w+)/,
2860
+ // Functions (standalone)
2861
+ /^(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/,
2862
+ // Arrow functions assigned to const/let
2863
+ /^(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[^=])\s*=>/,
2864
+ // Methods inside classes (indented)
2865
+ /^\s+(?:public|private|protected|static|async|readonly|\s)*(\w+)\s*\([^)]*\)\s*(?::\s*[\w<>\[\]|&\s]+)?\s*\{/,
2866
+ // Const exports
2867
+ /^(?:export\s+)?const\s+(\w+)\s*[:=]/
2868
+ ],
2869
+ javascript: [
2870
+ /^(?:export\s+)?(?:default\s+)?class\s+(\w+)/,
2871
+ /^(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/,
2872
+ /^(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[^=])\s*=>/,
2873
+ /^\s+(?:async\s+)?(\w+)\s*\([^)]*\)\s*\{/,
2874
+ /^(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=/
2875
+ ],
2876
+ python: [
2877
+ /^class\s+(\w+)(?:\([^)]*\))?:/,
2878
+ /^(?:async\s+)?def\s+(\w+)\s*\(/,
2879
+ /^(\w+)\s*=\s*(?:lambda|def)/
2880
+ ],
2881
+ java: [
2882
+ /^(?:public|private|protected)?\s*(?:static)?\s*(?:final)?\s*(?:abstract)?\s*class\s+(\w+)/,
2883
+ /^(?:public|private|protected)?\s*(?:static)?\s*(?:final)?\s*interface\s+(\w+)/,
2884
+ /^\s*(?:public|private|protected)?\s*(?:static)?\s*(?:final)?\s*(?:synchronized)?\s*(?:\w+(?:<[^>]+>)?)\s+(\w+)\s*\(/
2885
+ ],
2886
+ go: [
2887
+ /^type\s+(\w+)\s+struct\s*\{/,
2888
+ /^type\s+(\w+)\s+interface\s*\{/,
2889
+ /^func\s+(?:\([^)]+\)\s+)?(\w+)\s*\(/
2890
+ ],
2891
+ rust: [
2892
+ /^(?:pub\s+)?struct\s+(\w+)/,
2893
+ /^(?:pub\s+)?enum\s+(\w+)/,
2894
+ /^(?:pub\s+)?trait\s+(\w+)/,
2895
+ /^(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/,
2896
+ /^impl(?:<[^>]+>)?\s+(?:(\w+)|for\s+(\w+))/
2897
+ ],
2898
+ ruby: [
2899
+ /^class\s+(\w+)/,
2900
+ /^module\s+(\w+)/,
2901
+ /^def\s+(\w+)/
2902
+ ],
2903
+ php: [
2904
+ /^(?:abstract\s+)?class\s+(\w+)/,
2905
+ /^interface\s+(\w+)/,
2906
+ /^(?:public|private|protected)?\s*(?:static)?\s*function\s+(\w+)/
2907
+ ],
2908
+ csharp: [
2909
+ /^(?:public|private|protected|internal)?\s*(?:static)?\s*(?:partial)?\s*class\s+(\w+)/,
2910
+ /^(?:public|private|protected|internal)?\s*interface\s+(\w+)/,
2911
+ /^\s*(?:public|private|protected|internal)?\s*(?:static)?\s*(?:async)?\s*(?:\w+(?:<[^>]+>)?)\s+(\w+)\s*\(/
2912
+ ],
2913
+ swift: [
2914
+ /^(?:public|private|internal|open)?\s*class\s+(\w+)/,
2915
+ /^(?:public|private|internal)?\s*struct\s+(\w+)/,
2916
+ /^(?:public|private|internal)?\s*protocol\s+(\w+)/,
2917
+ /^(?:public|private|internal)?\s*(?:static)?\s*func\s+(\w+)/
2918
+ ],
2919
+ kotlin: [
2920
+ /^(?:open|abstract|sealed)?\s*class\s+(\w+)/,
2921
+ /^interface\s+(\w+)/,
2922
+ /^(?:private|public|internal)?\s*fun\s+(\w+)/
2923
+ ],
2924
+ scala: [
2925
+ /^(?:sealed\s+)?(?:abstract\s+)?class\s+(\w+)/,
2926
+ /^object\s+(\w+)/,
2927
+ /^trait\s+(\w+)/,
2928
+ /^def\s+(\w+)/
2929
+ ],
2930
+ c: [
2931
+ /^(?:static\s+)?(?:inline\s+)?(?:const\s+)?(?:\w+(?:\s*\*)*)\s+(\w+)\s*\([^)]*\)\s*\{/,
2932
+ /^typedef\s+struct\s*\w*\s*\{[^}]*\}\s*(\w+)/,
2933
+ /^struct\s+(\w+)\s*\{/
2934
+ ],
2935
+ cpp: [
2936
+ /^class\s+(\w+)/,
2937
+ /^(?:virtual\s+)?(?:\w+(?:\s*[*&])*)\s+(\w+)\s*\([^)]*\)\s*(?:const)?\s*(?:override)?\s*\{/,
2938
+ /^namespace\s+(\w+)\s*\{/
2939
+ ]
2940
+ };
2941
+ function detectLanguage(filepath) {
2942
+ const ext = path8.extname(filepath).toLowerCase();
2943
+ return LANGUAGE_MAP[ext] || "unknown";
2944
+ }
2945
+ function determineItemType(line, language) {
2946
+ const normalizedLine = line.trim().toLowerCase();
2947
+ if (normalizedLine.includes("class ")) return "class";
2948
+ if (normalizedLine.includes("interface ")) return "interface";
2949
+ if (normalizedLine.includes("type ")) return "type";
2950
+ if (normalizedLine.startsWith("import ")) return "import";
2951
+ if (normalizedLine.includes("export ") && !normalizedLine.includes("function") && !normalizedLine.includes("class")) return "export";
2952
+ if (line.startsWith(" ") || line.startsWith(" ")) {
2953
+ if (normalizedLine.includes("function") || normalizedLine.includes("def ") || normalizedLine.includes("fn ") || normalizedLine.includes("func ")) {
2954
+ return "method";
2910
2955
  }
2911
- runningCommands.set(commandId, entry);
2912
- cleanupOldCommands();
2913
- return {
2914
- success: true,
2915
- command_id: commandId,
2916
- command,
2917
- message: `Command started in background. Use command_status with id "${commandId}" to check progress.`
2918
- };
2919
- } catch (error) {
2920
- return {
2921
- success: false,
2922
- error: `Unexpected error: ${error.message}`
2923
- };
2956
+ if (/^\s+\w+\s*\(/.test(line)) return "method";
2957
+ }
2958
+ if (normalizedLine.includes("function ") || normalizedLine.includes("def ") || normalizedLine.includes("fn ") || normalizedLine.includes("func ")) {
2959
+ return "function";
2960
+ }
2961
+ if (normalizedLine.includes("const ") || normalizedLine.includes("let ") || normalizedLine.includes("var ")) {
2962
+ return normalizedLine.includes("=>") ? "function" : "const";
2924
2963
  }
2964
+ return "function";
2925
2965
  }
2926
- async function commandStatus(args) {
2927
- try {
2928
- const {
2929
- command_id,
2930
- wait_seconds = 0,
2931
- output_limit = 1e4
2932
- } = args;
2933
- if (!command_id) {
2934
- return {
2935
- success: false,
2936
- command_id: "",
2937
- status: "error",
2938
- error: "command_id is required"
2939
- };
2940
- }
2941
- const entry = runningCommands.get(command_id);
2942
- if (!entry) {
2943
- return {
2944
- success: false,
2945
- command_id,
2946
- status: "not_found",
2947
- error: `Command with id "${command_id}" not found. It may have expired or never existed.`
2948
- };
2949
- }
2950
- if (wait_seconds > 0 && entry.status === "running") {
2951
- await new Promise((resolve) => {
2952
- const checkInterval = setInterval(() => {
2953
- if (entry.status !== "running") {
2954
- clearInterval(checkInterval);
2955
- resolve();
2956
- }
2957
- }, 100);
2958
- setTimeout(() => {
2959
- clearInterval(checkInterval);
2960
- resolve();
2961
- }, wait_seconds * 1e3);
2962
- });
2963
- }
2964
- let stdout = entry.stdout;
2965
- let stderr = entry.stderr;
2966
- let truncated = false;
2967
- if (stdout.length > output_limit) {
2968
- stdout = "..." + stdout.substring(stdout.length - output_limit);
2969
- truncated = true;
2966
+ async function extractOutline(filepath, content, language) {
2967
+ const items = [];
2968
+ const lines = content.split("\n");
2969
+ const patterns = PATTERNS[language] || PATTERNS.javascript;
2970
+ let currentClass = null;
2971
+ let braceCount = 0;
2972
+ for (let i = 0; i < lines.length; i++) {
2973
+ const line = lines[i];
2974
+ const lineNumber = i + 1;
2975
+ const openBraces = (line.match(/\{/g) || []).length;
2976
+ const closeBraces = (line.match(/\}/g) || []).length;
2977
+ braceCount += openBraces - closeBraces;
2978
+ if (braceCount <= 0 && currentClass) {
2979
+ currentClass = null;
2970
2980
  }
2971
- if (stderr.length > output_limit) {
2972
- stderr = "..." + stderr.substring(stderr.length - output_limit);
2973
- truncated = true;
2981
+ for (const pattern of patterns) {
2982
+ const match = line.match(pattern);
2983
+ if (match) {
2984
+ const name = match[1] || match[2];
2985
+ if (!name) continue;
2986
+ const itemType = determineItemType(line, language);
2987
+ if (!itemType) continue;
2988
+ const exported = line.includes("export ");
2989
+ const isInsideClass = currentClass && (line.startsWith(" ") || line.startsWith(" "));
2990
+ const item = {
2991
+ type: itemType,
2992
+ name,
2993
+ line_start: lineNumber,
2994
+ signature: line.trim().substring(0, 120),
2995
+ exported
2996
+ };
2997
+ if (isInsideClass && itemType === "method" && currentClass) {
2998
+ item.parent = currentClass;
2999
+ }
3000
+ if (itemType === "class") {
3001
+ currentClass = name;
3002
+ }
3003
+ items.push(item);
3004
+ break;
3005
+ }
2974
3006
  }
2975
- const duration = entry.endTime ? (entry.endTime - entry.startTime) / 1e3 : (Date.now() - entry.startTime) / 1e3;
2976
- return {
2977
- success: true,
2978
- command_id,
2979
- status: entry.status,
2980
- stdout: stdout || void 0,
2981
- stderr: stderr || void 0,
2982
- exit_code: entry.exitCode,
2983
- duration_seconds: Math.round(duration * 10) / 10,
2984
- truncated
2985
- };
2986
- } catch (error) {
2987
- return {
2988
- success: false,
2989
- command_id: args.command_id || "",
2990
- status: "error",
2991
- error: `Unexpected error: ${error.message}`
2992
- };
2993
3007
  }
3008
+ return items;
2994
3009
  }
2995
- async function sendCommandInput(args) {
2996
- try {
2997
- const { command_id, input } = args;
2998
- if (!command_id || input === void 0) {
2999
- return {
3000
- success: false,
3001
- error: "command_id and input are required"
3002
- };
3003
- }
3004
- const entry = runningCommands.get(command_id);
3005
- if (!entry) {
3006
- return {
3007
- success: false,
3008
- error: `Command with id "${command_id}" not found`
3009
- };
3010
- }
3011
- if (entry.status !== "running" || !entry.process) {
3012
- return {
3013
- success: false,
3014
- error: `Command is not running (status: ${entry.status})`
3015
- };
3016
- }
3017
- entry.process.stdin?.write(input);
3018
- return {
3019
- success: true,
3020
- message: `Sent ${input.length} characters to command ${command_id}`
3021
- };
3022
- } catch (error) {
3023
- return {
3024
- success: false,
3025
- error: `Failed to send input: ${error.message}`
3026
- };
3010
+ function generateSummary(items, totalLines) {
3011
+ const counts = {};
3012
+ for (const item of items) {
3013
+ counts[item.type] = (counts[item.type] || 0) + 1;
3027
3014
  }
3015
+ const parts = [`${totalLines} lines`];
3016
+ if (counts.class) parts.push(`${counts.class} class${counts.class > 1 ? "es" : ""}`);
3017
+ if (counts.interface) parts.push(`${counts.interface} interface${counts.interface > 1 ? "s" : ""}`);
3018
+ if (counts.function) parts.push(`${counts.function} function${counts.function > 1 ? "s" : ""}`);
3019
+ if (counts.method) parts.push(`${counts.method} method${counts.method > 1 ? "s" : ""}`);
3020
+ if (counts.type) parts.push(`${counts.type} type${counts.type > 1 ? "s" : ""}`);
3021
+ if (counts.const) parts.push(`${counts.const} const${counts.const > 1 ? "s" : ""}`);
3022
+ return parts.join(", ");
3028
3023
  }
3029
- async function killCommand(args) {
3024
+ async function viewFileOutline(args) {
3030
3025
  try {
3031
- const { command_id } = args;
3032
- const entry = runningCommands.get(command_id);
3033
- if (!entry) {
3026
+ const { file_path } = args;
3027
+ if (!file_path || typeof file_path !== "string") {
3034
3028
  return {
3035
3029
  success: false,
3036
- error: `Command with id "${command_id}" not found`
3030
+ file_path: file_path || "",
3031
+ language: "unknown",
3032
+ total_lines: 0,
3033
+ items: [],
3034
+ summary: "",
3035
+ error: "file_path is required and must be a string"
3037
3036
  };
3038
3037
  }
3039
- if (entry.status !== "running" || !entry.process) {
3038
+ const resolvedPath = path8.resolve(file_path);
3039
+ let content;
3040
+ try {
3041
+ content = await fsPromises3.readFile(resolvedPath, "utf-8");
3042
+ } catch (error) {
3040
3043
  return {
3041
3044
  success: false,
3042
- error: `Command is not running (status: ${entry.status})`
3045
+ file_path: resolvedPath,
3046
+ language: "unknown",
3047
+ total_lines: 0,
3048
+ items: [],
3049
+ summary: "",
3050
+ error: error.code === "ENOENT" ? `File not found: ${resolvedPath}` : `Error reading file: ${error.message}`
3043
3051
  };
3044
3052
  }
3045
- entry.process.kill("SIGTERM");
3046
- entry.status = "killed";
3047
- entry.endTime = Date.now();
3053
+ const language = detectLanguage(resolvedPath);
3054
+ const lines = content.split("\n");
3055
+ const totalLines = lines.length;
3056
+ const items = await extractOutline(resolvedPath, content, language);
3048
3057
  return {
3049
3058
  success: true,
3050
- message: `Command ${command_id} killed`
3059
+ file_path: resolvedPath,
3060
+ language,
3061
+ total_lines: totalLines,
3062
+ items,
3063
+ summary: generateSummary(items, totalLines)
3051
3064
  };
3052
3065
  } catch (error) {
3053
3066
  return {
3054
3067
  success: false,
3055
- error: `Failed to kill command: ${error.message}`
3068
+ file_path: args.file_path || "",
3069
+ language: "unknown",
3070
+ total_lines: 0,
3071
+ items: [],
3072
+ summary: "",
3073
+ error: `Unexpected error: ${error.message}`
3056
3074
  };
3057
3075
  }
3058
3076
  }
3059
3077
 
3078
+ // src/app/agent/tool_invoker.ts
3079
+ init_async_command();
3080
+
3060
3081
  // src/app/agent/tools/natives/task_boundary.ts
3061
3082
  import path9 from "path";
3062
3083
  import { promises as fs7 } from "fs";
@@ -4107,8 +4128,13 @@ You MUST use \`command_status\` to get the output.
4107
4128
  [Step 1] shell_command({ command: "docker build -t myapp .", timeout: 600 })
4108
4129
  \u2192 { command_id: "cmd_004" }
4109
4130
 
4110
- [Step 2] command_status({ command_id: "cmd_004", wait_seconds: 300 })
4111
- \u2192 { status: "completed", exit_code: 0, truncated: true, stdout: "..." }
4131
+ [Step 2] command_status({ command_id: "cmd_004", wait_seconds: 10 })
4132
+ \u2192 { status: "running" }
4133
+
4134
+ [Step 3] message_notify_user("Building Docker image, please wait...")
4135
+
4136
+ [Step 4] command_status({ command_id: "cmd_004", wait_seconds: 10 })
4137
+ \u2192 { status: "completed", exit_code: 0, stdout: "Successfully built abc123" }
4112
4138
  \`\`\`
4113
4139
 
4114
4140
  **[WRONG] Never forget command_status:**
@@ -4117,6 +4143,11 @@ shell_command({ command: "npm install" })
4117
4143
  message_notify_user("Installed!") // WRONG: You don't know if it succeeded!
4118
4144
  \`\`\`
4119
4145
 
4146
+ **[WRONG] Never use long waits (blocks user):**
4147
+ \`\`\`
4148
+ command_status({ id: "...", wait_seconds: 300 }) // WRONG: User can't interrupt!
4149
+ \`\`\`
4150
+
4120
4151
  **[WRONG] Never use blocked commands:**
4121
4152
  \`\`\`
4122
4153
  shell_command({ command: "sudo apt install ..." }) // BLOCKED
@@ -4126,8 +4157,9 @@ shell_command({ command: "vim file.txt" }) // BLOCKED (interactive)
4126
4157
 
4127
4158
  ### Tips:
4128
4159
  - Always check \`exit_code\` - 0 means success, non-zero is error
4129
- - If \`status: "running"\`, call command_status again with longer wait
4130
- - If \`truncated: true\`, the output was too long - use \`| head\` or \`| tail\`
4160
+ - Use wait_seconds: 5-10 for polling (allows user interrupts)
4161
+ - If \`status: "running"\`, notify user and poll again
4162
+ - If \`truncated: true\`, output was too long - use \`| head\` or \`| tail\`
4131
4163
  - Use \`send_command_input\` for commands that need input (y/n prompts)
4132
4164
  - Use \`kill_command\` to stop a stuck command
4133
4165
  </shell_command>
@@ -6208,6 +6240,61 @@ var AppComponent = ({ eventBus: eventBus2, sessionId: sessionId2 }) => {
6208
6240
  ]);
6209
6241
  return;
6210
6242
  }
6243
+ if (text.startsWith("!")) {
6244
+ const command = text.slice(1).trim();
6245
+ if (!command) {
6246
+ setIsProcessing(false);
6247
+ return;
6248
+ }
6249
+ setHistory((prev) => [
6250
+ ...prev,
6251
+ {
6252
+ id: prev.length,
6253
+ component: /* @__PURE__ */ jsxs15(Box16, { marginBottom: 1, children: [
6254
+ /* @__PURE__ */ jsx16(Text15, { color: "white", bold: true, children: "$ " }),
6255
+ /* @__PURE__ */ jsx16(Text15, { color: "white", children: command })
6256
+ ] })
6257
+ }
6258
+ ]);
6259
+ Promise.resolve().then(() => (init_async_command(), async_command_exports)).then(async ({ runCommandAsync: runCommandAsync2 }) => {
6260
+ try {
6261
+ const result = await runCommandAsync2({ command, cwd: workdir });
6262
+ if (result.success && result.command_id) {
6263
+ const contextMessage = `[User executed shell command directly]
6264
+ Command: ${command}
6265
+ Command ID: ${result.command_id}
6266
+
6267
+ Please use command_status to check the result and report back to the user.`;
6268
+ agentInstance.current?.processTurn({ content: contextMessage });
6269
+ } else {
6270
+ setHistory((prev) => [
6271
+ ...prev,
6272
+ {
6273
+ id: prev.length,
6274
+ component: /* @__PURE__ */ jsxs15(Text15, { color: "red", children: [
6275
+ "Failed to execute: ",
6276
+ result.error || result.message
6277
+ ] })
6278
+ }
6279
+ ]);
6280
+ setIsProcessing(false);
6281
+ }
6282
+ } catch (err) {
6283
+ setHistory((prev) => [
6284
+ ...prev,
6285
+ {
6286
+ id: prev.length,
6287
+ component: /* @__PURE__ */ jsxs15(Text15, { color: "red", children: [
6288
+ "Error: ",
6289
+ err.message
6290
+ ] })
6291
+ }
6292
+ ]);
6293
+ setIsProcessing(false);
6294
+ }
6295
+ });
6296
+ return;
6297
+ }
6211
6298
  setIsProcessing(true);
6212
6299
  const displayText = text.length > 1e4 ? text.substring(0, 1e4) + "..." : text;
6213
6300
  setHistory((prev) => [