@bike4mind/cli 0.2.11 → 0.2.12-ctrl-c-clear-input.17412

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4,9 +4,9 @@ import {
4
4
  getEffectiveApiKey,
5
5
  getOpenWeatherKey,
6
6
  getSerperKey
7
- } from "./chunk-ZHCX7BLB.js";
8
- import "./chunk-ID4NS6KF.js";
9
- import "./chunk-RJFNIMVH.js";
7
+ } from "./chunk-XM52K32A.js";
8
+ import "./chunk-JXROCVSM.js";
9
+ import "./chunk-7N7AZPLK.js";
10
10
  import {
11
11
  BFLImageService,
12
12
  BaseStorage,
@@ -15,10 +15,10 @@ import {
15
15
  OpenAIBackend,
16
16
  OpenAIImageService,
17
17
  XAIImageService
18
- } from "./chunk-BU5TP3BH.js";
18
+ } from "./chunk-INYINU6X.js";
19
19
  import {
20
20
  Logger
21
- } from "./chunk-AMDXHL6S.js";
21
+ } from "./chunk-OCYRD7D6.js";
22
22
  import "./chunk-BPFEGDC7.js";
23
23
  import {
24
24
  AiEvents,
@@ -74,9 +74,6 @@ import {
74
74
  b4mLLMTools,
75
75
  getMcpProviderMetadata
76
76
  } from "./chunk-OHR7UCTC.js";
77
- import {
78
- __require
79
- } from "./chunk-PDX44BCA.js";
80
77
 
81
78
  // src/index.tsx
82
79
  import React17, { useState as useState8, useEffect as useEffect3, useCallback, useRef as useRef2 } from "react";
@@ -182,11 +179,11 @@ function CustomTextInput({
182
179
  setCursorOffset(Math.min(value.length, cursorOffset + 1));
183
180
  return;
184
181
  }
185
- if (!key.ctrl && !key.meta && input.length === 1) {
182
+ if (!key.ctrl && !key.meta && input.length > 0) {
186
183
  const sanitizedInput = input === "\r" ? "\n" : input;
187
184
  const newValue = value.slice(0, cursorOffset) + sanitizedInput + value.slice(cursorOffset);
188
185
  onChange(newValue);
189
- setCursorOffset(cursorOffset + 1);
186
+ setCursorOffset(cursorOffset + sanitizedInput.length);
190
187
  }
191
188
  },
192
189
  { isActive: !disabled }
@@ -713,7 +710,7 @@ var ImageInputDetector = class {
713
710
  if (!this.looksLikeFilePath(input)) return null;
714
711
  let filepath = input.trim();
715
712
  filepath = filepath.replace(/^["']|["']$/g, "");
716
- filepath = filepath.replace(/\\\s/g, " ");
713
+ filepath = filepath.replace(/\\(.)/g, "$1");
717
714
  if (!existsSync(filepath)) return null;
718
715
  const stats = statSync2(filepath);
719
716
  if (!stats.isFile()) return null;
@@ -740,7 +737,15 @@ var ImageInputDetector = class {
740
737
  */
741
738
  static looksLikeFilePath(input) {
742
739
  const trimmed = input.trim();
743
- if (trimmed.startsWith("/") || trimmed.match(/^[a-zA-Z]:\\/)) {
740
+ if (trimmed.startsWith("/")) {
741
+ const hasMultipleSegments = trimmed.indexOf("/", 1) !== -1;
742
+ const hasImageExtension = this.SUPPORTED_EXTENSIONS.some((ext) => trimmed.toLowerCase().endsWith(ext));
743
+ if (hasMultipleSegments || hasImageExtension) {
744
+ return true;
745
+ }
746
+ return false;
747
+ }
748
+ if (trimmed.match(/^[a-zA-Z]:\\/)) {
744
749
  return true;
745
750
  }
746
751
  if (trimmed.startsWith("~")) {
@@ -768,7 +773,74 @@ var ImageInputDetector = class {
768
773
  }
769
774
  };
770
775
 
776
+ // src/store/index.ts
777
+ import { create } from "zustand";
778
+ var useCliStore = create((set) => ({
779
+ // Session state
780
+ session: null,
781
+ setSession: (session) => set({ session }),
782
+ addMessage: (message) => set((state) => {
783
+ if (!state.session) return state;
784
+ return {
785
+ session: {
786
+ ...state.session,
787
+ messages: [...state.session.messages, message],
788
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
789
+ }
790
+ };
791
+ }),
792
+ // Pending messages
793
+ pendingMessages: [],
794
+ addPendingMessage: (message) => set((state) => ({ pendingMessages: [...state.pendingMessages, message] })),
795
+ updatePendingMessage: (index, message) => set((state) => {
796
+ const updated = [...state.pendingMessages];
797
+ updated[index] = message;
798
+ return { pendingMessages: updated };
799
+ }),
800
+ clearPendingMessages: () => set({ pendingMessages: [] }),
801
+ completePendingMessage: (index, finalMessage) => set((state) => {
802
+ const pending = [...state.pendingMessages];
803
+ pending.splice(index, 1);
804
+ const session = state.session;
805
+ if (!session) return { pendingMessages: pending };
806
+ return {
807
+ pendingMessages: pending,
808
+ session: {
809
+ ...session,
810
+ messages: [...session.messages, finalMessage],
811
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
812
+ }
813
+ };
814
+ }),
815
+ // UI state
816
+ isThinking: false,
817
+ setIsThinking: (thinking) => set({ isThinking: thinking }),
818
+ // Input state (for Ctrl+C clearing)
819
+ inputValue: "",
820
+ setInputValue: (value) => set({ inputValue: value }),
821
+ clearInput: () => set({ inputValue: "" }),
822
+ // Permission prompt
823
+ permissionPrompt: null,
824
+ setPermissionPrompt: (prompt) => set({ permissionPrompt: prompt }),
825
+ // Config editor
826
+ showConfigEditor: false,
827
+ setShowConfigEditor: (show) => set({ showConfigEditor: show }),
828
+ // Exit handling
829
+ exitRequested: false,
830
+ setExitRequested: (requested) => set({ exitRequested: requested })
831
+ }));
832
+
771
833
  // src/components/InputPrompt.tsx
834
+ function looksLikeFilePath(input) {
835
+ const trimmed = input.trim();
836
+ if (trimmed.startsWith("/")) {
837
+ const hasMultipleSegments = trimmed.indexOf("/", 1) !== -1;
838
+ if (hasMultipleSegments) return true;
839
+ }
840
+ if (trimmed.startsWith("~")) return true;
841
+ if (trimmed.match(/^[a-zA-Z]:\\/)) return true;
842
+ return false;
843
+ }
772
844
  function findAtTrigger(value) {
773
845
  for (let i = value.length - 1; i >= 0; i--) {
774
846
  if (value[i] === "@") {
@@ -795,7 +867,9 @@ function InputPrompt({
795
867
  onPrefillConsumed,
796
868
  onBashModeChange
797
869
  }) {
798
- const [value, setValue] = useState2("");
870
+ const value = useCliStore((state) => state.inputValue);
871
+ const setInputValue = useCliStore((state) => state.setInputValue);
872
+ const setValue = setInputValue;
799
873
  const [selectedIndex, setSelectedIndex] = useState2(0);
800
874
  const [historyIndex, setHistoryIndex] = useState2(-1);
801
875
  const [tempInput, setTempInput] = useState2("");
@@ -812,7 +886,7 @@ function InputPrompt({
812
886
  useEffect(() => {
813
887
  onBashModeChange?.(isBashMode);
814
888
  }, [isBashMode, onBashModeChange]);
815
- const shouldShowCommandAutocomplete = value.startsWith("/") && !disabled && !fileAutocomplete?.active;
889
+ const shouldShowCommandAutocomplete = value.startsWith("/") && !disabled && !fileAutocomplete?.active && !looksLikeFilePath(value);
816
890
  const commandQuery = shouldShowCommandAutocomplete ? value.slice(1) : "";
817
891
  const filteredCommands = useMemo(() => {
818
892
  if (!shouldShowCommandAutocomplete) return [];
@@ -851,10 +925,11 @@ function InputPrompt({
851
925
  if (shouldShowCommandAutocomplete && filteredCommands.length > 0) {
852
926
  if (key.upArrow) {
853
927
  setSelectedIndex((prev) => prev > 0 ? prev - 1 : filteredCommands.length - 1);
928
+ return;
854
929
  } else if (key.downArrow) {
855
930
  setSelectedIndex((prev) => prev < filteredCommands.length - 1 ? prev + 1 : 0);
931
+ return;
856
932
  }
857
- return;
858
933
  }
859
934
  if (!shouldShowCommandAutocomplete && !fileAutocomplete?.active && history.length > 0) {
860
935
  if (key.upArrow) {
@@ -992,59 +1067,6 @@ var ThoughtStream = React6.memo(function ThoughtStream2({ isThinking }) {
992
1067
  return /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column", gap: 1 }, isThinking && /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(Text6, { color: "yellow" }, /* @__PURE__ */ React6.createElement(Spinner, { type: "dots" })), /* @__PURE__ */ React6.createElement(Text6, null, " Thinking...")));
993
1068
  });
994
1069
 
995
- // src/store/index.ts
996
- import { create } from "zustand";
997
- var useCliStore = create((set) => ({
998
- // Session state
999
- session: null,
1000
- setSession: (session) => set({ session }),
1001
- addMessage: (message) => set((state) => {
1002
- if (!state.session) return state;
1003
- return {
1004
- session: {
1005
- ...state.session,
1006
- messages: [...state.session.messages, message],
1007
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1008
- }
1009
- };
1010
- }),
1011
- // Pending messages
1012
- pendingMessages: [],
1013
- addPendingMessage: (message) => set((state) => ({ pendingMessages: [...state.pendingMessages, message] })),
1014
- updatePendingMessage: (index, message) => set((state) => {
1015
- const updated = [...state.pendingMessages];
1016
- updated[index] = message;
1017
- return { pendingMessages: updated };
1018
- }),
1019
- clearPendingMessages: () => set({ pendingMessages: [] }),
1020
- completePendingMessage: (index, finalMessage) => set((state) => {
1021
- const pending = [...state.pendingMessages];
1022
- pending.splice(index, 1);
1023
- const session = state.session;
1024
- if (!session) return { pendingMessages: pending };
1025
- return {
1026
- pendingMessages: pending,
1027
- session: {
1028
- ...session,
1029
- messages: [...session.messages, finalMessage],
1030
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1031
- }
1032
- };
1033
- }),
1034
- // UI state
1035
- isThinking: false,
1036
- setIsThinking: (thinking) => set({ isThinking: thinking }),
1037
- // Permission prompt
1038
- permissionPrompt: null,
1039
- setPermissionPrompt: (prompt) => set({ permissionPrompt: prompt }),
1040
- // Config editor
1041
- showConfigEditor: false,
1042
- setShowConfigEditor: (show) => set({ showConfigEditor: show }),
1043
- // Exit handling
1044
- exitRequested: false,
1045
- setExitRequested: (requested) => set({ exitRequested: requested })
1046
- }));
1047
-
1048
1070
  // src/components/AgentThinking.tsx
1049
1071
  var AgentThinking = React7.memo(function AgentThinking2() {
1050
1072
  const isThinking = useCliStore((state) => state.isThinking);
@@ -1333,7 +1355,7 @@ function MessageItem({ message }) {
1333
1355
  const isUser = message.role === "user";
1334
1356
  return /* @__PURE__ */ React10.createElement(Box9, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React10.createElement(Box9, null, /* @__PURE__ */ React10.createElement(Text9, { bold: true, color: isUser ? "cyan" : "green" }, isUser ? "\u{1F464} You" : "\u{1F916} Assistant"), isUser && /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " \u2022 ", new Date(message.timestamp).toLocaleTimeString())), isUser && message.content && /* @__PURE__ */ React10.createElement(Box9, { paddingLeft: 2 }, /* @__PURE__ */ React10.createElement(Text9, { backgroundColor: "whiteBright", color: "black" }, " ", message.content, " ")), !isUser && message.metadata?.steps && message.metadata.steps.length > 0 && /* @__PURE__ */ React10.createElement(Box9, { paddingLeft: 2, marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React10.createElement(Text9, { dimColor: true, bold: true }, "\u{1F527} Agent Reasoning Trace (", message.metadata.steps.filter((s) => s.type === "action").length, " ", "tools used, ", message.metadata.steps.length, " total steps)", message.metadata.tokenUsage && ` \u2022 ${message.metadata.tokenUsage.total} tokens`), /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, "Step types: ", message.metadata.steps.map((s) => s.type).join(", ")), message.metadata.steps.map((step, idx) => {
1335
1357
  if (step.type === "thought") {
1336
- return /* @__PURE__ */ React10.createElement(Box9, { key: idx, paddingLeft: 2, marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React10.createElement(Text9, { color: "blue" }, "\u{1F4AD} Thought:"), /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, ` ${step.content.slice(0, 200)}${step.content.length > 200 ? "..." : ""}`));
1358
+ return /* @__PURE__ */ React10.createElement(Box9, { key: idx, paddingLeft: 2, marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React10.createElement(Text9, { color: "blue" }, "\u{1F4AD} Thought:"), /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, ` ${step.content}`));
1337
1359
  }
1338
1360
  if (step.type === "action") {
1339
1361
  const toolName = step.metadata?.toolName || "unknown";
@@ -1358,7 +1380,7 @@ function isNameSuffix(value) {
1358
1380
 
1359
1381
  // src/utils/processFileReferences.ts
1360
1382
  var FILE_REFERENCE_REGEX = /(?:^|\s)@([^\s@]+)/g;
1361
- function looksLikeFilePath(ref) {
1383
+ function looksLikeFilePath2(ref) {
1362
1384
  if (ref.includes("/") || ref.includes(path2.sep)) {
1363
1385
  return true;
1364
1386
  }
@@ -1381,7 +1403,7 @@ function extractFileReferences(message) {
1381
1403
  let match;
1382
1404
  while ((match = FILE_REFERENCE_REGEX.exec(message)) !== null) {
1383
1405
  const ref = match[1];
1384
- if (looksLikeFilePath(ref)) {
1406
+ if (looksLikeFilePath2(ref)) {
1385
1407
  references.push(ref);
1386
1408
  }
1387
1409
  }
@@ -3093,6 +3115,21 @@ ${options.context}` : this.getSystemPrompt()
3093
3115
  let finalAnswer = "";
3094
3116
  let reachedMaxIterations = false;
3095
3117
  while (iterations < maxIterations) {
3118
+ if (options.signal?.aborted) {
3119
+ this.context.logger.info("[ReActAgent] Operation aborted by user");
3120
+ const result2 = {
3121
+ finalAnswer: "Interrupted",
3122
+ steps: this.steps,
3123
+ completionInfo: {
3124
+ totalTokens: this.totalTokens,
3125
+ iterations,
3126
+ toolCalls: this.toolCallCount,
3127
+ reachedMaxIterations: false
3128
+ }
3129
+ };
3130
+ this.emit("complete", result2);
3131
+ return result2;
3132
+ }
3096
3133
  iterations++;
3097
3134
  this.context.logger.debug(`[ReActAgent] Starting iteration ${iterations}/${maxIterations}`);
3098
3135
  let iterationComplete = false;
@@ -3106,7 +3143,8 @@ ${options.context}` : this.getSystemPrompt()
3106
3143
  stream: false,
3107
3144
  tools: this.context.tools,
3108
3145
  maxTokens,
3109
- temperature
3146
+ temperature,
3147
+ abortSignal: options.signal
3110
3148
  },
3111
3149
  async (texts, completionInfo) => {
3112
3150
  for (const text of texts) {
@@ -3249,6 +3287,9 @@ ${options.context}` : this.getSystemPrompt()
3249
3287
  this.emit("final_answer", finalStep);
3250
3288
  }
3251
3289
  }
3290
+ if (options.signal?.aborted && !finalAnswer) {
3291
+ finalAnswer = "Interrupted";
3292
+ }
3252
3293
  const result = {
3253
3294
  finalAnswer,
3254
3295
  steps: this.steps,
@@ -3356,6 +3397,70 @@ Remember: You are an autonomous AGENT. Act independently and solve problems proa
3356
3397
  }
3357
3398
  };
3358
3399
 
3400
+ // src/core/prompts.ts
3401
+ var TOOL_GREP_SEARCH = "grep_search";
3402
+ var TOOL_GLOB_FILES = "glob_files";
3403
+ var TOOL_FILE_READ = "file_read";
3404
+ var TOOL_EDIT_LOCAL_FILE = "edit_local_file";
3405
+ var TOOL_CREATE_FILE = "create_file";
3406
+ var TOOL_BASH_EXECUTE = "bash_execute";
3407
+ var TOOL_SUBAGENT_DELEGATE = "subagent_delegate";
3408
+ var TOOL_WRITE_TODOS = "write_todos";
3409
+ var EXPLORE_SUBAGENT_TYPE = "explore";
3410
+ function buildCoreSystemPrompt(contextSection = "") {
3411
+ return `You are an autonomous AI assistant with access to tools. Your job is to help users by taking action and solving problems proactively.
3412
+
3413
+ CORE BEHAVIOR:
3414
+ - Be proactive: Take action instead of asking for permission or clarification
3415
+ - Make smart assumptions: If unclear, choose the most reasonable interpretation
3416
+ - Use conversation history: Reference previous exchanges to understand context
3417
+ - Complete tasks fully: Don't just show what to do - actually do it
3418
+
3419
+ FOR SOFTWARE ENGINEERING TASKS:
3420
+ When requested to perform tasks like fixing bugs, adding features, refactoring, or explaining code, follow this sequence:
3421
+ 1. **Understand:** Think about the user's request and the relevant codebase context. Use '${TOOL_GREP_SEARCH}' and '${TOOL_GLOB_FILES}' search tools extensively (in parallel if independent) to understand file structures, existing code patterns, and conventions. Use '${TOOL_FILE_READ}' to understand context and validate any assumptions you may have. If you need to read multiple files, you should make multiple parallel calls to '${TOOL_FILE_READ}'.
3422
+ When the task involves **complex refactoring, codebase exploration or system-wide analysis**, your **first and primary action** must be to delegate to the '${EXPLORE_SUBAGENT_TYPE}' agent using the '${TOOL_SUBAGENT_DELEGATE}' tool. Use it to build a comprehensive understanding of the code, its structure, and dependencies. For **simple, targeted searches** (like finding a specific function name, file path, or variable declaration), you should use '${TOOL_GREP_SEARCH}' or '${TOOL_GLOB_FILES}' directly.
3423
+ 2. **Plan:** Build a coherent and grounded (based on the understanding in step 1) plan for how you intend to resolve the user's task. Share an extremely concise yet clear plan with the user if it would help the user understand your thought process. As part of the plan, you should use an iterative development process that includes writing unit tests to verify your changes. Use output logs or debug statements as part of this process to arrive at a solution.
3424
+ If '${EXPLORE_SUBAGENT_TYPE}' subagent was used, do not ignore the output of the agent, you must use it as the foundation of your plan. For complex tasks, break them down into smaller, manageable subtasks and use the \`${TOOL_WRITE_TODOS}\` tool to track your progress. Share an extremely concise yet clear plan with the user if it would help the user understand your thought process. As part of the plan, you should use an iterative development process that includes writing unit tests to verify your changes. Use output logs or debug statements as part of this process to arrive at a solution.
3425
+ 3. **Implement:** Use the available tools (e.g., '${TOOL_EDIT_LOCAL_FILE}', '${TOOL_CREATE_FILE}', '${TOOL_BASH_EXECUTE}' ...) to act on the plan, strictly adhering to the project's established conventions.
3426
+ 4. **Verify (Tests):** If applicable and feasible, verify the changes using the project's testing procedures. Identify the correct test commands and frameworks by examining 'README' files, build/package configuration (e.g., 'package.json'), or existing test execution patterns. NEVER assume standard test commands. When executing test commands, prefer "run once" or "CI" modes to ensure the command terminates after completion.
3427
+ 5. **Verify (Standards):** VERY IMPORTANT: After making code changes, execute the project-specific build, linting and type-checking commands (e.g., 'tsc', 'npm run lint', 'ruff check .') that you have identified for this project (or obtained from the user). This ensures code quality and adherence to standards.
3428
+ 6. **Finalize:** After all verification passes, consider the task complete. Do not remove or revert any changes or created files (like tests). Await the user's next instruction.
3429
+
3430
+ SUBAGENT DELEGATION:
3431
+ - You have access to specialized subagents via the ${TOOL_SUBAGENT_DELEGATE} tool
3432
+ - Use subagents for focused exploration, planning, or review tasks:
3433
+ * explore: Fast read-only codebase search (e.g., "find all auth files", "locate API endpoints")
3434
+ * plan: Break down complex tasks into actionable steps
3435
+ * review: Analyze code quality and identify issues
3436
+ - Subagents keep the main conversation clean and run faster with optimized models
3437
+ - Delegate when you need to search extensively or analyze code structure
3438
+
3439
+ FOR GENERAL TASKS:
3440
+ - Use available tools to get information (weather, web search, calculations, etc.)
3441
+ - When user asks follow-up questions, use conversation context to understand what they're referring to
3442
+ - If user asks "how about X?" after a previous question, apply the same question type to X
3443
+
3444
+ ## Shell tool output token efficiency:
3445
+ IT IS CRITICAL TO FOLLOW THESE GUIDELINES TO AVOID EXCESSIVE TOKEN CONSUMPTION.
3446
+
3447
+ - Always prefer command flags that reduce output verbosity when using '${TOOL_BASH_EXECUTE}'.
3448
+ - Aim to minimize tool output tokens while still capturing necessary information.
3449
+ - If a command is expected to produce a lot of output, use quiet or silent flags where available and appropriate.
3450
+ - Always consider the trade-off between output verbosity and the need for information. If a command's full output is essential for understanding the result, avoid overly aggressive quieting that might obscure important details.
3451
+ - If a command does not have quiet/silent flags or for commands with potentially long output that may not be useful, redirect stdout and stderr to temp files in the project's temporary directory. For example: 'command > <temp_dir>/out.log 2> <temp_dir>/err.log'.
3452
+ - After the command runs, inspect the temp files (e.g. '<temp_dir>/out.log' and '<temp_dir>/err.log') using commands like 'grep', 'tail', 'head', ... (or platform equivalents). Remove the temp files when done.
3453
+
3454
+ EXAMPLES:
3455
+ - "what should I wear in Texas?" \u2192 use weather tool for Texas
3456
+ - "how about Japan?" \u2192 use weather tool for Japan (applying same question from context)
3457
+ - "enhance README" \u2192 ${TOOL_FILE_READ} \u2192 generate \u2192 ${TOOL_EDIT_LOCAL_FILE}
3458
+ - "what packages installed?" \u2192 ${TOOL_GLOB_FILES} "**/package.json" \u2192 ${TOOL_FILE_READ}
3459
+ - "find all components using React hooks" \u2192 ${TOOL_SUBAGENT_DELEGATE}(task="find all components using React hooks", type="explore")
3460
+
3461
+ Remember: Use context from previous messages to understand follow-up questions.${contextSection}`;
3462
+ }
3463
+
3359
3464
  // ../../b4m-core/packages/services/dist/src/referService/generateCodes.js
3360
3465
  import { randomBytes } from "crypto";
3361
3466
  import range from "lodash/range.js";
@@ -8707,98 +8812,80 @@ var createFileTool = {
8707
8812
  };
8708
8813
 
8709
8814
  // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/globFiles/index.js
8710
- import { readdir } from "fs/promises";
8711
- import { statSync as statSync5 } from "fs";
8815
+ import { glob } from "glob";
8816
+ import { stat } from "fs/promises";
8712
8817
  import path9 from "path";
8713
- function matchGlob(filePath, pattern) {
8714
- let regexPattern = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, ".").replace(/\{([^}]+)\}/g, (_, options) => `(${options.split(",").join("|")})`).replace(/\[([^\]]+)\]/g, "[$1]");
8715
- regexPattern = regexPattern.replace(/\{\{GLOBSTAR\}\}\//g, "(.*/)?").replace(/\/\{\{GLOBSTAR\}\}/g, "(/.*)?").replace(/\{\{GLOBSTAR\}\}/g, ".*");
8716
- const regex = new RegExp(`^${regexPattern}$`);
8717
- return regex.test(filePath);
8718
- }
8719
- async function walkDir(dir, pattern, includeHidden, maxResults, basePath = "") {
8720
- const matches = [];
8721
- try {
8722
- const entries = await readdir(dir, { withFileTypes: true });
8723
- for (const entry of entries) {
8724
- if (matches.length >= maxResults) {
8725
- break;
8726
- }
8727
- if (!includeHidden && entry.name.startsWith(".")) {
8728
- continue;
8729
- }
8730
- const relativePath = basePath ? path9.join(basePath, entry.name) : entry.name;
8731
- const fullPath = path9.join(dir, entry.name);
8732
- if (matchGlob(relativePath, pattern)) {
8733
- matches.push(relativePath);
8734
- }
8735
- if (entry.isDirectory() && (pattern.includes("**") || pattern.includes("/"))) {
8736
- const subMatches = await walkDir(fullPath, pattern, includeHidden, maxResults - matches.length, relativePath);
8737
- matches.push(...subMatches);
8738
- }
8739
- }
8740
- } catch (error) {
8741
- }
8742
- return matches;
8743
- }
8744
- async function listFiles(params) {
8745
- const { pattern, cwd: relativeCwd, includeHidden = false, maxResults = 1e3 } = params;
8818
+ var DEFAULT_IGNORE_PATTERNS2 = [
8819
+ "**/node_modules/**",
8820
+ "**/.git/**",
8821
+ "**/dist/**",
8822
+ "**/build/**",
8823
+ "**/.next/**",
8824
+ "**/coverage/**",
8825
+ "**/.turbo/**",
8826
+ "**/.sst/**",
8827
+ "**/*.min.js",
8828
+ "**/*.min.css"
8829
+ ];
8830
+ async function findFiles(params) {
8831
+ const { pattern, dir_path, case_sensitive = true, respect_git_ignore = true } = params;
8746
8832
  const baseCwd = process.cwd();
8747
- const targetCwd = relativeCwd ? path9.resolve(baseCwd, path9.normalize(relativeCwd)) : baseCwd;
8748
- if (!targetCwd.startsWith(baseCwd)) {
8749
- throw new Error(`Access denied: Cannot list files outside of current working directory`);
8750
- }
8751
- const matches = await walkDir(targetCwd, pattern, includeHidden, maxResults);
8833
+ const targetDir = dir_path ? path9.resolve(baseCwd, path9.normalize(dir_path)) : baseCwd;
8834
+ if (!targetDir.startsWith(baseCwd)) {
8835
+ throw new Error(`Access denied: Cannot search outside of current working directory`);
8836
+ }
8837
+ const ignorePatterns = respect_git_ignore ? DEFAULT_IGNORE_PATTERNS2 : [];
8838
+ const matches = await glob(pattern, {
8839
+ cwd: targetDir,
8840
+ dot: false,
8841
+ // Don't match hidden files by default
8842
+ ignore: ignorePatterns,
8843
+ absolute: true,
8844
+ nodir: true,
8845
+ // Only return files, not directories
8846
+ nocase: !case_sensitive,
8847
+ // Case-insensitive if requested
8848
+ maxDepth: 20
8849
+ // Reasonable depth limit
8850
+ });
8752
8851
  if (matches.length === 0) {
8753
- return `No files found matching pattern: ${pattern}`;
8852
+ return `No files found matching pattern: ${pattern}${dir_path ? ` in ${dir_path}` : ""}`;
8754
8853
  }
8755
- const filesInfo = [];
8756
- for (const match of matches) {
8854
+ const filesWithStats = [];
8855
+ for (const filePath of matches) {
8757
8856
  try {
8758
- const fullPath = path9.join(targetCwd, match);
8759
- const stats = statSync5(fullPath);
8760
- filesInfo.push({
8761
- path: match,
8762
- size: stats.size,
8763
- isDirectory: stats.isDirectory(),
8764
- modified: stats.mtime.toISOString()
8857
+ const stats = await stat(filePath);
8858
+ filesWithStats.push({
8859
+ path: filePath,
8860
+ mtime: stats.mtimeMs
8765
8861
  });
8766
- } catch (error) {
8862
+ } catch {
8767
8863
  continue;
8768
8864
  }
8769
8865
  }
8770
- const truncated = matches.length >= maxResults;
8771
- const summary = `Found ${filesInfo.length} file(s)${truncated ? ` (truncated to ${maxResults})` : ""} matching: ${pattern}
8772
- `;
8773
- const cwdInfo = relativeCwd ? `Directory: ${relativeCwd}
8774
- ` : "";
8775
- const filesList = filesInfo.map((file) => {
8776
- const typeIcon = file.isDirectory ? "\u{1F4C1}" : "\u{1F4C4}";
8777
- const sizeStr = file.isDirectory ? "" : ` (${formatBytes(file.size)})`;
8778
- return `${typeIcon} ${file.path}${sizeStr}`;
8779
- }).join("\n");
8780
- return `${summary}${cwdInfo}
8866
+ filesWithStats.sort((a, b) => b.mtime - a.mtime);
8867
+ const MAX_RESULTS = 500;
8868
+ const truncated = filesWithStats.length > MAX_RESULTS;
8869
+ const results = truncated ? filesWithStats.slice(0, MAX_RESULTS) : filesWithStats;
8870
+ const summary = `Found ${filesWithStats.length} file(s)${truncated ? ` (showing first ${MAX_RESULTS})` : ""} matching: ${pattern}`;
8871
+ const dirInfo = dir_path ? `
8872
+ Directory: ${dir_path}` : "";
8873
+ const filesList = results.map((file) => path9.relative(baseCwd, file.path)).join("\n");
8874
+ return `${summary}${dirInfo}
8875
+
8781
8876
  ${filesList}`;
8782
8877
  }
8783
- function formatBytes(bytes) {
8784
- if (bytes === 0)
8785
- return "0 B";
8786
- const k = 1024;
8787
- const sizes = ["B", "KB", "MB", "GB"];
8788
- const i = Math.floor(Math.log(bytes) / Math.log(k));
8789
- return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
8790
- }
8791
8878
  var globFilesTool = {
8792
8879
  name: "glob_files",
8793
8880
  implementation: (context) => ({
8794
8881
  toolFn: async (value) => {
8795
8882
  const params = value;
8796
- context.logger.info("\u{1F50D} GlobFiles: Listing files", {
8883
+ context.logger.info("\u{1F50D} GlobFiles: Finding files", {
8797
8884
  pattern: params.pattern,
8798
- cwd: params.cwd || "."
8885
+ dir_path: params.dir_path || "."
8799
8886
  });
8800
8887
  try {
8801
- const result = await listFiles(params);
8888
+ const result = await findFiles(params);
8802
8889
  context.logger.info("\u2705 GlobFiles: Success");
8803
8890
  return result;
8804
8891
  } catch (error) {
@@ -8808,25 +8895,25 @@ var globFilesTool = {
8808
8895
  },
8809
8896
  toolSchema: {
8810
8897
  name: "glob_files",
8811
- description: "List files and directories matching a glob pattern. Supports wildcards like *, **, ?, and character ranges. Restricted to current working directory for security.",
8898
+ description: "Efficiently finds files matching specific glob patterns (e.g., `src/**/*.ts`, `**/*.md`), returning absolute paths sorted by modification time (newest first). Use this to locate files by name or extension when you need to discover or explore files in a project.",
8812
8899
  parameters: {
8813
8900
  type: "object",
8814
8901
  properties: {
8815
8902
  pattern: {
8816
8903
  type: "string",
8817
- description: 'Glob pattern to match files (e.g., "*.ts", "src/**/*.tsx", "test/*.{js,ts}"). Use ** for recursive directory search.'
8904
+ description: 'The glob pattern to match against file paths. Supports wildcards (*, **), character ranges ([abc]), and brace expansion ({ts,tsx}). Examples: "*.ts" for TypeScript files in current dir, "src/**/*.tsx" for all TSX files in src tree, "**/*.{js,ts}" for all JS/TS files.'
8818
8905
  },
8819
- cwd: {
8906
+ dir_path: {
8820
8907
  type: "string",
8821
- description: "Directory to search in, relative to current working directory (optional). Defaults to current directory."
8908
+ description: "Optional: The absolute path to the directory to search in. If not specified, uses the current working directory. Must be within the current working directory."
8822
8909
  },
8823
- includeHidden: {
8910
+ case_sensitive: {
8824
8911
  type: "boolean",
8825
- description: "Include hidden files and directories (starting with .). Defaults to false."
8912
+ description: "Optional: Whether the pattern matching should be case-sensitive. Defaults to true. Set to false for case-insensitive matching on case-sensitive file systems."
8826
8913
  },
8827
- maxResults: {
8828
- type: "number",
8829
- description: "Maximum number of results to return (default: 1000). Prevents overwhelming output."
8914
+ respect_git_ignore: {
8915
+ type: "boolean",
8916
+ description: "Optional: Whether to respect common ignore patterns (node_modules, .git, dist, build, etc.). Defaults to true. Set to false to include all files."
8830
8917
  }
8831
8918
  },
8832
8919
  required: ["pattern"]
@@ -8836,158 +8923,144 @@ var globFilesTool = {
8836
8923
  };
8837
8924
 
8838
8925
  // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/grepSearch/index.js
8839
- import { readdir as readdir2 } from "fs/promises";
8840
- import { readFileSync as readFileSync3, statSync as statSync6 } from "fs";
8926
+ import { globStream } from "glob";
8927
+ import { readFile, stat as stat2 } from "fs/promises";
8841
8928
  import path10 from "path";
8842
8929
  var MAX_FILE_SIZE3 = 10 * 1024 * 1024;
8843
- function matchGlob2(filePath, pattern) {
8844
- const regexPattern = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\{\{GLOBSTAR\}\}/g, ".*").replace(/\?/g, ".").replace(/\{([^}]+)\}/g, (_, options) => `(${options.split(",").join("|")})`).replace(/\[([^\]]+)\]/g, "[$1]");
8845
- const regex = new RegExp(`^${regexPattern}$`);
8846
- return regex.test(filePath);
8847
- }
8848
- async function walkDir2(dir, pattern, maxResults, basePath = "") {
8849
- const matches = [];
8930
+ var DEFAULT_IGNORE_PATTERNS3 = [
8931
+ "**/node_modules/**",
8932
+ "**/.git/**",
8933
+ "**/dist/**",
8934
+ "**/build/**",
8935
+ "**/.next/**",
8936
+ "**/coverage/**",
8937
+ "**/*.min.js",
8938
+ "**/*.min.css",
8939
+ "**/package-lock.json",
8940
+ "**/pnpm-lock.yaml",
8941
+ "**/yarn.lock"
8942
+ ];
8943
+ async function isBinaryFile2(filePath) {
8850
8944
  try {
8851
- const entries = await readdir2(dir, { withFileTypes: true });
8852
- for (const entry of entries) {
8853
- if (matches.length >= maxResults) {
8854
- break;
8855
- }
8856
- if (entry.name.startsWith(".")) {
8857
- continue;
8858
- }
8859
- const relativePath = basePath ? path10.join(basePath, entry.name) : entry.name;
8860
- const fullPath = path10.join(dir, entry.name);
8861
- if (matchGlob2(relativePath, pattern)) {
8862
- matches.push(relativePath);
8863
- }
8864
- if (entry.isDirectory() && (pattern.includes("**") || pattern.includes("/"))) {
8865
- const subMatches = await walkDir2(fullPath, pattern, maxResults - matches.length, relativePath);
8866
- matches.push(...subMatches);
8867
- }
8868
- }
8869
- } catch (error) {
8945
+ const { openSync, readSync, closeSync } = await import("fs");
8946
+ const buffer = Buffer.alloc(8192);
8947
+ const fd = openSync(filePath, "r");
8948
+ const bytesRead = readSync(fd, buffer, 0, 8192, 0);
8949
+ closeSync(fd);
8950
+ const chunk = buffer.slice(0, bytesRead);
8951
+ return chunk.includes(0);
8952
+ } catch {
8953
+ return true;
8870
8954
  }
8871
- return matches;
8955
+ }
8956
+ function isPathWithinWorkspace(targetPath, baseCwd) {
8957
+ const resolvedTarget = path10.resolve(targetPath);
8958
+ const resolvedBase = path10.resolve(baseCwd);
8959
+ return resolvedTarget.startsWith(resolvedBase);
8872
8960
  }
8873
8961
  async function searchFiles2(params) {
8874
- const { pattern, filePattern = "**/*", cwd: relativeCwd, caseSensitive = false, maxResults = 100, contextLines = 0 } = params;
8962
+ const { pattern, dir_path, include } = params;
8875
8963
  const baseCwd = process.cwd();
8876
- const targetCwd = relativeCwd ? path10.resolve(baseCwd, path10.normalize(relativeCwd)) : baseCwd;
8877
- if (!targetCwd.startsWith(baseCwd)) {
8878
- throw new Error(`Access denied: Cannot search files outside of current working directory`);
8964
+ const targetDir = dir_path ? path10.resolve(baseCwd, dir_path) : baseCwd;
8965
+ if (!isPathWithinWorkspace(targetDir, baseCwd)) {
8966
+ throw new Error(`Path validation failed: "${dir_path}" resolves outside the allowed workspace directory`);
8967
+ }
8968
+ try {
8969
+ const stats = await stat2(targetDir);
8970
+ if (!stats.isDirectory()) {
8971
+ throw new Error(`Path is not a directory: ${dir_path}`);
8972
+ }
8973
+ } catch (error) {
8974
+ if (error.code === "ENOENT") {
8975
+ throw new Error(`Path does not exist: ${dir_path}`);
8976
+ }
8977
+ throw error;
8879
8978
  }
8880
8979
  let searchRegex;
8881
8980
  try {
8882
- const flags = caseSensitive ? "g" : "gi";
8883
- searchRegex = new RegExp(pattern, flags);
8981
+ searchRegex = new RegExp(pattern, "i");
8884
8982
  } catch (error) {
8885
8983
  throw new Error(`Invalid regex pattern: ${pattern}`);
8886
8984
  }
8887
- const filesToSearch = await walkDir2(targetCwd, filePattern, 1e4);
8888
- const filteredFiles = [];
8889
- for (const match of filesToSearch) {
8890
- const fullPath = path10.join(targetCwd, match);
8985
+ const globPattern = include || "**/*";
8986
+ const filesStream = globStream(globPattern, {
8987
+ cwd: targetDir,
8988
+ dot: false,
8989
+ // Skip hidden files
8990
+ ignore: DEFAULT_IGNORE_PATTERNS3,
8991
+ absolute: true,
8992
+ nodir: true
8993
+ // Only match files, not directories
8994
+ });
8995
+ const allMatches = [];
8996
+ let filesSearched = 0;
8997
+ const maxMatches = 500;
8998
+ for await (const filePath of filesStream) {
8999
+ if (allMatches.length >= maxMatches) {
9000
+ break;
9001
+ }
9002
+ const fileAbsPath = filePath;
8891
9003
  try {
8892
- const stats = statSync6(fullPath);
8893
- if (stats.isDirectory() || stats.size > MAX_FILE_SIZE3) {
9004
+ const stats = await stat2(fileAbsPath);
9005
+ if (stats.size > MAX_FILE_SIZE3) {
8894
9006
  continue;
8895
9007
  }
8896
- if (isBinaryFile2(fullPath)) {
9008
+ if (await isBinaryFile2(fileAbsPath)) {
8897
9009
  continue;
8898
9010
  }
8899
- filteredFiles.push(match);
8900
- } catch {
8901
- continue;
8902
- }
8903
- }
8904
- if (filteredFiles.length === 0) {
8905
- return `No files found matching pattern: ${filePattern}`;
8906
- }
8907
- const matches = [];
8908
- let filesSearched = 0;
8909
- for (const file of filteredFiles) {
8910
- if (matches.length >= maxResults) {
8911
- break;
8912
- }
8913
- filesSearched++;
8914
- const fullPath = path10.join(targetCwd, file);
8915
- try {
8916
- const content = readFileSync3(fullPath, "utf-8");
8917
- const lines = content.split("\n");
8918
- for (let i = 0; i < lines.length; i++) {
8919
- if (matches.length >= maxResults) {
8920
- break;
8921
- }
8922
- const line = lines[i];
8923
- if (searchRegex.test(line)) {
8924
- const match = {
8925
- file,
8926
- line: i + 1,
8927
- // 1-indexed line numbers
8928
- content: line.trim()
8929
- };
8930
- if (contextLines > 0) {
8931
- const before = [];
8932
- const after = [];
8933
- for (let j = Math.max(0, i - contextLines); j < i; j++) {
8934
- before.push(lines[j]);
8935
- }
8936
- for (let j = i + 1; j <= Math.min(lines.length - 1, i + contextLines); j++) {
8937
- after.push(lines[j]);
8938
- }
8939
- match.context = { before, after };
8940
- }
8941
- matches.push(match);
9011
+ filesSearched++;
9012
+ const content = await readFile(fileAbsPath, "utf8");
9013
+ const lines = content.split(/\r?\n/);
9014
+ lines.forEach((line, index) => {
9015
+ if (allMatches.length < maxMatches && searchRegex.test(line)) {
9016
+ allMatches.push({
9017
+ filePath: path10.relative(targetDir, fileAbsPath) || path10.basename(fileAbsPath),
9018
+ lineNumber: index + 1,
9019
+ line
9020
+ });
8942
9021
  }
8943
- searchRegex.lastIndex = 0;
9022
+ });
9023
+ } catch (readError) {
9024
+ const errCode = readError.code;
9025
+ if (errCode !== "ENOENT" && errCode !== "EACCES") {
9026
+ console.debug(`Could not read ${fileAbsPath}: ${readError.message}`);
8944
9027
  }
8945
- } catch {
8946
- continue;
8947
9028
  }
8948
9029
  }
8949
- if (matches.length === 0) {
8950
- return `No matches found for pattern: ${pattern}
8951
- Searched ${filesSearched} file(s)`;
9030
+ const searchDirDisplay = dir_path || ".";
9031
+ const filterInfo = include ? ` (filter: "${include}")` : "";
9032
+ if (allMatches.length === 0) {
9033
+ return `No matches found for pattern "${pattern}" in "${searchDirDisplay}"${filterInfo}.
9034
+ Searched ${filesSearched} file(s).`;
8952
9035
  }
8953
- const truncated = matches.length >= maxResults;
8954
- const summary = `Found ${matches.length} match(es)${truncated ? ` (truncated to ${maxResults})` : ""} in ${filesSearched} file(s)
9036
+ const matchesByFile = allMatches.reduce((acc, match) => {
9037
+ if (!acc[match.filePath]) {
9038
+ acc[match.filePath] = [];
9039
+ }
9040
+ acc[match.filePath].push(match);
9041
+ acc[match.filePath].sort((a, b) => a.lineNumber - b.lineNumber);
9042
+ return acc;
9043
+ }, {});
9044
+ const matchCount = allMatches.length;
9045
+ const matchTerm = matchCount === 1 ? "match" : "matches";
9046
+ const truncated = matchCount >= maxMatches;
9047
+ let result = `Found ${matchCount} ${matchTerm}${truncated ? " (truncated)" : ""} for pattern "${pattern}" in "${searchDirDisplay}"${filterInfo}:
8955
9048
  `;
8956
- const patternInfo = `Pattern: ${pattern}${caseSensitive ? "" : " (case-insensitive)"}
9049
+ result += `Searched ${filesSearched} file(s)
9050
+ ---
8957
9051
  `;
8958
- const cwdInfo = relativeCwd ? `Directory: ${relativeCwd}
8959
- ` : "";
8960
- const matchesList = matches.map((match) => {
8961
- let result = `
8962
- \u{1F4C4} ${match.file}:${match.line}
8963
- ${match.content}`;
8964
- if (match.context) {
8965
- if (match.context.before.length > 0) {
8966
- result = `${result}
8967
- Context before:
8968
- ${match.context.before.map((l) => ` ${l}`).join("\n")}`;
8969
- }
8970
- if (match.context.after.length > 0) {
8971
- result = `${result}
8972
- Context after:
8973
- ${match.context.after.map((l) => ` ${l}`).join("\n")}`;
8974
- }
8975
- }
8976
- return result;
8977
- }).join("\n");
8978
- return `${summary}${patternInfo}${cwdInfo}${matchesList}`;
8979
- }
8980
- function isBinaryFile2(filePath) {
8981
- try {
8982
- const buffer = Buffer.alloc(8192);
8983
- const fd = __require("fs").openSync(filePath, "r");
8984
- const bytesRead = __require("fs").readSync(fd, buffer, 0, 8192, 0);
8985
- __require("fs").closeSync(fd);
8986
- const chunk = buffer.slice(0, bytesRead);
8987
- return chunk.includes(0);
8988
- } catch {
8989
- return true;
9052
+ for (const filePath in matchesByFile) {
9053
+ result += `File: ${filePath}
9054
+ `;
9055
+ matchesByFile[filePath].forEach((match) => {
9056
+ const trimmedLine = match.line.trim();
9057
+ const displayLine = trimmedLine.length > 200 ? trimmedLine.slice(0, 200) + "..." : trimmedLine;
9058
+ result += `L${match.lineNumber}: ${displayLine}
9059
+ `;
9060
+ });
9061
+ result += "---\n";
8990
9062
  }
9063
+ return result.trim();
8991
9064
  }
8992
9065
  var grepSearchTool = {
8993
9066
  name: "grep_search",
@@ -8996,8 +9069,8 @@ var grepSearchTool = {
8996
9069
  const params = value;
8997
9070
  context.logger.info("\u{1F50D} GrepSearch: Searching files", {
8998
9071
  pattern: params.pattern,
8999
- filePattern: params.filePattern || "**/*",
9000
- cwd: params.cwd || "."
9072
+ dir_path: params.dir_path || ".",
9073
+ include: params.include || "**/*"
9001
9074
  });
9002
9075
  try {
9003
9076
  const result = await searchFiles2(params);
@@ -9010,33 +9083,21 @@ var grepSearchTool = {
9010
9083
  },
9011
9084
  toolSchema: {
9012
9085
  name: "grep_search",
9013
- description: "Search for a regex pattern in file contents. Similar to grep command. Searches text files only, excluding binary files. Restricted to current working directory for security.",
9086
+ description: "Searches for a regular expression pattern within the content of files in a specified directory (or current working directory). Can filter files by a glob pattern. Returns the lines containing matches, along with their file paths and line numbers.",
9014
9087
  parameters: {
9015
9088
  type: "object",
9016
9089
  properties: {
9017
9090
  pattern: {
9018
9091
  type: "string",
9019
- description: "Regular expression pattern to search for in file contents"
9092
+ description: "The regular expression (regex) pattern to search for within file contents (e.g., 'function\\s+myFunction', 'import\\s+\\{.*\\}\\s+from\\s+.*')."
9020
9093
  },
9021
- filePattern: {
9094
+ dir_path: {
9022
9095
  type: "string",
9023
- description: 'Glob pattern to filter which files to search (optional, default: "**/*"). Example: "src/**/*.ts" to search only TypeScript files in src directory.'
9096
+ description: "Optional: The absolute path to the directory to search within. If omitted, searches the current working directory."
9024
9097
  },
9025
- cwd: {
9098
+ include: {
9026
9099
  type: "string",
9027
- description: "Directory to search in, relative to current working directory (optional). Defaults to current directory."
9028
- },
9029
- caseSensitive: {
9030
- type: "boolean",
9031
- description: "Whether the search should be case-sensitive (default: false)"
9032
- },
9033
- maxResults: {
9034
- type: "number",
9035
- description: "Maximum number of matches to return (default: 100). Prevents overwhelming output."
9036
- },
9037
- contextLines: {
9038
- type: "number",
9039
- description: "Number of context lines to show before and after each match (default: 0). Similar to grep -C option."
9100
+ description: "Optional: A glob pattern to filter which files are searched (e.g., '*.js', '*.{ts,tsx}', 'src/**'). If omitted, searches all files (respecting potential global ignores)."
9040
9101
  }
9041
9102
  },
9042
9103
  required: ["pattern"]
@@ -9047,7 +9108,7 @@ var grepSearchTool = {
9047
9108
 
9048
9109
  // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/deleteFile/index.js
9049
9110
  import { promises as fs10 } from "fs";
9050
- import { existsSync as existsSync6, statSync as statSync7 } from "fs";
9111
+ import { existsSync as existsSync6, statSync as statSync5 } from "fs";
9051
9112
  import path11 from "path";
9052
9113
  async function deleteFile(params) {
9053
9114
  const { path: filePath, recursive = false } = params;
@@ -9060,7 +9121,7 @@ async function deleteFile(params) {
9060
9121
  if (!existsSync6(resolvedPath)) {
9061
9122
  throw new Error(`File or directory not found: ${filePath}`);
9062
9123
  }
9063
- const stats = statSync7(resolvedPath);
9124
+ const stats = statSync5(resolvedPath);
9064
9125
  const isDirectory = stats.isDirectory();
9065
9126
  const size = stats.size;
9066
9127
  if (isDirectory && !recursive) {
@@ -9081,7 +9142,7 @@ var deleteFileTool = {
9081
9142
  toolFn: async (value) => {
9082
9143
  const params = value;
9083
9144
  const resolvedPath = path11.resolve(process.cwd(), path11.normalize(params.path));
9084
- const isDirectory = existsSync6(resolvedPath) && statSync7(resolvedPath).isDirectory();
9145
+ const isDirectory = existsSync6(resolvedPath) && statSync5(resolvedPath).isDirectory();
9085
9146
  context.logger.info(`\u{1F5D1}\uFE0F DeleteFile: Deleting ${isDirectory ? "directory" : "file"}`, {
9086
9147
  path: params.path,
9087
9148
  recursive: params.recursive
@@ -9857,7 +9918,7 @@ var ToolErrorType;
9857
9918
 
9858
9919
  // src/utils/diffPreview.ts
9859
9920
  import * as Diff from "diff";
9860
- import { readFile } from "fs/promises";
9921
+ import { readFile as readFile2 } from "fs/promises";
9861
9922
  import { existsSync as existsSync8 } from "fs";
9862
9923
  async function generateFileDiffPreview(args) {
9863
9924
  try {
@@ -9872,7 +9933,7 @@ ${preview}${hasMore ? `
9872
9933
 
9873
9934
  ... (${lines2.length - 20} more lines)` : ""}`;
9874
9935
  }
9875
- const currentContent = await readFile(args.path, "utf-8");
9936
+ const currentContent = await readFile2(args.path, "utf-8");
9876
9937
  const patch = Diff.createPatch(
9877
9938
  args.path,
9878
9939
  currentContent,
@@ -10781,6 +10842,82 @@ async function loadContextFiles(projectDir) {
10781
10842
  };
10782
10843
  }
10783
10844
 
10845
+ // src/utils/formatStep.ts
10846
+ var MAX_INPUT_LENGTH = 500;
10847
+ var formatStep = (step) => {
10848
+ if (step.type === "action" && step.metadata?.toolInput) {
10849
+ let parsedInput = step.metadata.toolInput;
10850
+ if (typeof parsedInput === "string") {
10851
+ try {
10852
+ parsedInput = JSON.parse(parsedInput);
10853
+ } catch {
10854
+ }
10855
+ }
10856
+ if (step.metadata.toolName === "edit_local_file") {
10857
+ const pathOnly = typeof parsedInput === "object" && parsedInput !== null && "path" in parsedInput ? { path: parsedInput.path } : parsedInput;
10858
+ return {
10859
+ ...step,
10860
+ metadata: {
10861
+ ...step.metadata,
10862
+ toolInput: pathOnly
10863
+ }
10864
+ };
10865
+ }
10866
+ if (step.metadata.toolName === "write_todos") {
10867
+ const todos = typeof parsedInput === "object" && parsedInput !== null && "todos" in parsedInput ? parsedInput.todos : null;
10868
+ const count = Array.isArray(todos) ? todos.length : 0;
10869
+ return {
10870
+ ...step,
10871
+ metadata: {
10872
+ ...step.metadata,
10873
+ toolInput: `${count} todo${count !== 1 ? "s" : ""}`
10874
+ }
10875
+ };
10876
+ }
10877
+ const inputStr = typeof parsedInput === "string" ? parsedInput : JSON.stringify(parsedInput);
10878
+ if (inputStr.length > MAX_INPUT_LENGTH) {
10879
+ const truncatedInput = inputStr.slice(0, MAX_INPUT_LENGTH) + `... (${inputStr.length - MAX_INPUT_LENGTH} more chars)`;
10880
+ return {
10881
+ ...step,
10882
+ metadata: {
10883
+ ...step.metadata,
10884
+ toolInput: truncatedInput
10885
+ }
10886
+ };
10887
+ }
10888
+ }
10889
+ if (step.type === "observation" && step.content) {
10890
+ if (step.metadata?.toolName === "file_read") {
10891
+ const lineCount = step.content.split("\n").length;
10892
+ return {
10893
+ ...step,
10894
+ content: `Read ${lineCount} line${lineCount !== 1 ? "s" : ""}`
10895
+ };
10896
+ }
10897
+ if (step.metadata?.toolName === "grep_search") {
10898
+ const match = step.content.match(/^Found (\d+) file\(s\)/);
10899
+ if (match) {
10900
+ const count = parseInt(match[1], 10);
10901
+ return {
10902
+ ...step,
10903
+ content: `Found ${count} file${count !== 1 ? "s" : ""}`
10904
+ };
10905
+ }
10906
+ }
10907
+ if (step.metadata?.toolName === "glob_files") {
10908
+ const match = step.content.match(/^Found (\d+) file\(s\)/);
10909
+ if (match) {
10910
+ const count = parseInt(match[1], 10);
10911
+ return {
10912
+ ...step,
10913
+ content: `Found ${count} file${count !== 1 ? "s" : ""}`
10914
+ };
10915
+ }
10916
+ }
10917
+ }
10918
+ return step;
10919
+ };
10920
+
10784
10921
  // src/utils/argumentSubstitution.ts
10785
10922
  function substituteArguments(template, args) {
10786
10923
  let result = template;
@@ -11248,6 +11385,10 @@ var ServerLlmBackend = class {
11248
11385
  */
11249
11386
  async complete(model, messages, options, callback) {
11250
11387
  logger.debug(`[ServerLlmBackend] Starting complete() with model: ${model}`);
11388
+ if (options.abortSignal?.aborted) {
11389
+ logger.debug("[ServerLlmBackend] Request aborted before start");
11390
+ return;
11391
+ }
11251
11392
  return new Promise(async (resolve3, reject) => {
11252
11393
  try {
11253
11394
  logger.debug("[ServerLlmBackend] Making streaming request...");
@@ -11348,7 +11489,25 @@ var ServerLlmBackend = class {
11348
11489
  }
11349
11490
  }
11350
11491
  });
11492
+ if (options.abortSignal) {
11493
+ const abortHandler = () => {
11494
+ logger.debug("[ServerLlmBackend] Abort signal received, destroying stream");
11495
+ response.data.destroy();
11496
+ resolve3();
11497
+ };
11498
+ if (options.abortSignal.aborted) {
11499
+ abortHandler();
11500
+ return;
11501
+ }
11502
+ options.abortSignal.addEventListener("abort", abortHandler, { once: true });
11503
+ response.data.on("close", () => {
11504
+ options.abortSignal?.removeEventListener("abort", abortHandler);
11505
+ });
11506
+ }
11351
11507
  response.data.on("data", (chunk) => {
11508
+ if (options.abortSignal?.aborted) {
11509
+ return;
11510
+ }
11352
11511
  parser.feed(chunk.toString());
11353
11512
  });
11354
11513
  response.data.on("end", () => {
@@ -11360,9 +11519,23 @@ var ServerLlmBackend = class {
11360
11519
  }
11361
11520
  });
11362
11521
  response.data.on("error", (error) => {
11522
+ if (options.abortSignal?.aborted) {
11523
+ resolve3();
11524
+ return;
11525
+ }
11363
11526
  reject(error);
11364
11527
  });
11365
11528
  } catch (error) {
11529
+ if (options.abortSignal?.aborted) {
11530
+ logger.debug("[ServerLlmBackend] Request was aborted, resolving gracefully");
11531
+ resolve3();
11532
+ return;
11533
+ }
11534
+ if (isAxiosError(error) && error.code === "ERR_CANCELED") {
11535
+ logger.debug("[ServerLlmBackend] Request was canceled, resolving gracefully");
11536
+ resolve3();
11537
+ return;
11538
+ }
11366
11539
  logger.error("LLM completion failed", error);
11367
11540
  if (isAxiosError(error)) {
11368
11541
  logger.debug(
@@ -11513,8 +11686,10 @@ var ServerLlmBackend = class {
11513
11686
  logger.debug(` Body: ${logger.formatBytes(bodySize)}`);
11514
11687
  logger.debug(` Preview: ${bodyStr.substring(0, 200)}`);
11515
11688
  const response = await axiosInstance.post(this.completionsEndpoint, requestBody, {
11516
- responseType: "stream"
11689
+ responseType: "stream",
11517
11690
  // Auth header is automatically injected by ApiClient interceptor
11691
+ // Pass abort signal to cancel request if user presses ESC
11692
+ signal: options.abortSignal
11518
11693
  });
11519
11694
  logger.debug(`\u2190 ${response.status} ${response.statusText}`);
11520
11695
  return response;
@@ -11658,7 +11833,7 @@ import { isAxiosError as isAxiosError2 } from "axios";
11658
11833
  // package.json
11659
11834
  var package_default = {
11660
11835
  name: "@bike4mind/cli",
11661
- version: "0.2.11",
11836
+ version: "0.2.12-ctrl-c-clear-input.17412+46c1f7133",
11662
11837
  type: "module",
11663
11838
  description: "Interactive CLI tool for Bike4Mind with ReAct agents",
11664
11839
  license: "UNLICENSED",
@@ -11695,7 +11870,8 @@ var package_default = {
11695
11870
  test: "vitest run",
11696
11871
  "test:watch": "vitest",
11697
11872
  start: "node dist/index.js",
11698
- prepublishOnly: "pnpm build"
11873
+ prepublishOnly: "pnpm build",
11874
+ postinstall: `node -e "try { require('better-sqlite3') } catch(e) { if(e.message.includes('bindings')) { console.log('\\n\u26A0\uFE0F Rebuilding better-sqlite3 native bindings...'); require('child_process').execSync('pnpm rebuild better-sqlite3', {stdio:'inherit'}) } }"`
11699
11875
  },
11700
11876
  dependencies: {
11701
11877
  "@anthropic-ai/sdk": "^0.22.0",
@@ -11728,6 +11904,7 @@ var package_default = {
11728
11904
  "eventsource-parser": "^3.0.6",
11729
11905
  "file-type": "^18.7.0",
11730
11906
  "fuse.js": "^7.1.0",
11907
+ glob: "^13.0.0",
11731
11908
  "gray-matter": "^4.0.3",
11732
11909
  ink: "^6.5.1",
11733
11910
  "ink-select-input": "^6.2.0",
@@ -11761,11 +11938,11 @@ var package_default = {
11761
11938
  zustand: "^4.5.4"
11762
11939
  },
11763
11940
  devDependencies: {
11764
- "@bike4mind/agents": "workspace:*",
11765
- "@bike4mind/common": "workspace:*",
11766
- "@bike4mind/mcp": "workspace:*",
11767
- "@bike4mind/services": "workspace:*",
11768
- "@bike4mind/utils": "workspace:*",
11941
+ "@bike4mind/agents": "0.1.0",
11942
+ "@bike4mind/common": "2.41.1-ctrl-c-clear-input.17412+46c1f7133",
11943
+ "@bike4mind/mcp": "1.21.1-ctrl-c-clear-input.17412+46c1f7133",
11944
+ "@bike4mind/services": "2.36.1-ctrl-c-clear-input.17412+46c1f7133",
11945
+ "@bike4mind/utils": "2.1.6-ctrl-c-clear-input.17412+46c1f7133",
11769
11946
  "@types/better-sqlite3": "^7.6.13",
11770
11947
  "@types/diff": "^5.0.9",
11771
11948
  "@types/jsonwebtoken": "^9.0.4",
@@ -11777,7 +11954,8 @@ var package_default = {
11777
11954
  tsx: "^4.21.0",
11778
11955
  typescript: "^5.9.3",
11779
11956
  vitest: "^3.2.4"
11780
- }
11957
+ },
11958
+ gitHead: "46c1f7133ac46ff30ab650cda7ec3c332e35fb4c"
11781
11959
  };
11782
11960
 
11783
11961
  // src/config/constants.ts
@@ -12133,6 +12311,131 @@ function createSubagentDelegateTool(orchestrator, parentSessionId) {
12133
12311
  };
12134
12312
  }
12135
12313
 
12314
+ // src/tools/writeTodosTool.ts
12315
+ var VALID_STATUSES = ["pending", "in_progress", "completed", "cancelled"];
12316
+ function validateTodos(todos) {
12317
+ if (!Array.isArray(todos)) {
12318
+ throw new Error("write_todos: todos must be an array");
12319
+ }
12320
+ if (todos.length === 0) {
12321
+ throw new Error("write_todos: todos array cannot be empty");
12322
+ }
12323
+ let inProgressCount = 0;
12324
+ const validatedTodos = todos.map((item, index) => {
12325
+ if (!item || typeof item !== "object") {
12326
+ throw new Error(`write_todos: item at index ${index} must be an object`);
12327
+ }
12328
+ const { description, status } = item;
12329
+ if (typeof description !== "string" || description.trim() === "") {
12330
+ throw new Error(`write_todos: item at index ${index} must have a non-empty description`);
12331
+ }
12332
+ if (typeof status !== "string" || !VALID_STATUSES.includes(status)) {
12333
+ throw new Error(
12334
+ `write_todos: item at index ${index} has invalid status "${status}". Must be one of: ${VALID_STATUSES.join(", ")}`
12335
+ );
12336
+ }
12337
+ if (status === "in_progress") {
12338
+ inProgressCount++;
12339
+ }
12340
+ return {
12341
+ description: description.trim(),
12342
+ status
12343
+ };
12344
+ });
12345
+ if (inProgressCount > 1) {
12346
+ throw new Error(`write_todos: only one task can be 'in_progress' at a time, but found ${inProgressCount}`);
12347
+ }
12348
+ return validatedTodos;
12349
+ }
12350
+ function formatTodosOutput(todos) {
12351
+ const statusEmoji = {
12352
+ pending: "\u2B1C",
12353
+ in_progress: "\u{1F504}",
12354
+ completed: "\u2705",
12355
+ cancelled: "\u274C"
12356
+ };
12357
+ const lines = todos.map((todo, index) => {
12358
+ const emoji = statusEmoji[todo.status];
12359
+ return `${index + 1}. ${emoji} [${todo.status}] ${todo.description}`;
12360
+ });
12361
+ return lines.join("\n");
12362
+ }
12363
+ function createWriteTodosTool(todoStore) {
12364
+ return {
12365
+ toolFn: async (args) => {
12366
+ const params = args;
12367
+ const validatedTodos = validateTodos(params.todos);
12368
+ todoStore.todos = validatedTodos;
12369
+ if (todoStore.onUpdate) {
12370
+ todoStore.onUpdate(validatedTodos);
12371
+ }
12372
+ const output = formatTodosOutput(validatedTodos);
12373
+ return `Todo list updated successfully:
12374
+
12375
+ ${output}`;
12376
+ },
12377
+ toolSchema: {
12378
+ name: "write_todos",
12379
+ description: `Create or update a list of subtasks for complex multi-step requests.
12380
+
12381
+ **When to use this tool:**
12382
+ - When handling complex requests that require multiple steps
12383
+ - To break down a large task into smaller, manageable subtasks
12384
+ - To track progress through a multi-step workflow
12385
+
12386
+ **Important guidelines:**
12387
+ - Call this tool early when receiving complex requests
12388
+ - Update the list immediately when starting, completing, or cancelling tasks
12389
+ - Only ONE task should be 'in_progress' at a time
12390
+ - Never batch updates - update as soon as state changes
12391
+
12392
+ **Status values:**
12393
+ - pending: Task not yet started
12394
+ - in_progress: Currently working on this task (only ONE allowed)
12395
+ - completed: Task finished successfully
12396
+ - cancelled: Task will not be completed
12397
+
12398
+ **Example:**
12399
+ If asked to "create a new React component with tests", break it down:
12400
+ 1. Create component file \u2192 in_progress
12401
+ 2. Add component logic \u2192 pending
12402
+ 3. Create test file \u2192 pending
12403
+ 4. Write unit tests \u2192 pending`,
12404
+ parameters: {
12405
+ type: "object",
12406
+ properties: {
12407
+ todos: {
12408
+ type: "array",
12409
+ description: "Complete list of todo items. This replaces any existing list. Each item needs a description and status.",
12410
+ items: {
12411
+ type: "object",
12412
+ properties: {
12413
+ description: {
12414
+ type: "string",
12415
+ description: "Clear, concise description of the task"
12416
+ },
12417
+ status: {
12418
+ type: "string",
12419
+ enum: ["pending", "in_progress", "completed", "cancelled"],
12420
+ description: "Current status of the task. Only ONE task can be 'in_progress' at a time."
12421
+ }
12422
+ },
12423
+ required: ["description", "status"]
12424
+ }
12425
+ }
12426
+ },
12427
+ required: ["todos"]
12428
+ }
12429
+ }
12430
+ };
12431
+ }
12432
+ function createTodoStore(onUpdate) {
12433
+ return {
12434
+ todos: [],
12435
+ onUpdate
12436
+ };
12437
+ }
12438
+
12136
12439
  // src/index.tsx
12137
12440
  process.removeAllListeners("warning");
12138
12441
  process.on("warning", (warning) => {
@@ -12164,7 +12467,8 @@ function CliApp() {
12164
12467
  trustLocationSelector: null,
12165
12468
  rewindSelector: null,
12166
12469
  sessionSelector: null,
12167
- orchestrator: null
12470
+ orchestrator: null,
12471
+ abortController: null
12168
12472
  });
12169
12473
  const [isInitialized, setIsInitialized] = useState8(false);
12170
12474
  const [initError, setInitError] = useState8(null);
@@ -12210,15 +12514,28 @@ function CliApp() {
12210
12514
  }, 100);
12211
12515
  }, [state.session, state.sessionStore, state.mcpManager, state.agent, state.imageStore]);
12212
12516
  useInput6((input, key) => {
12517
+ if (key.escape) {
12518
+ if (state.abortController) {
12519
+ logger.debug("[ABORT] ESC pressed - aborting current operation...");
12520
+ state.abortController.abort();
12521
+ setState((prev) => ({ ...prev, abortController: null }));
12522
+ useCliStore.getState().setIsThinking(false);
12523
+ }
12524
+ return;
12525
+ }
12213
12526
  if (key.ctrl && input === "c") {
12214
12527
  const now = Date.now();
12215
12528
  if (exitTimestamp && now - exitTimestamp < EXIT_TIMEOUT_MS) {
12216
- logger.debug("[EXIT Second Ctrl+C - cleaning up and exiting...");
12529
+ logger.debug("[EXIT] Second Ctrl+C - cleaning up and exiting...");
12217
12530
  performCleanup().then(() => {
12218
12531
  exit();
12219
12532
  });
12220
12533
  } else {
12221
- logger.debug("[EXIT First Ctrl+C - press again to exit");
12534
+ logger.debug("[EXIT] First Ctrl+C - press again to exit");
12535
+ const currentInput = useCliStore.getState().inputValue;
12536
+ if (currentInput.length > 0) {
12537
+ useCliStore.getState().clearInput();
12538
+ }
12222
12539
  exitTimestamp = now;
12223
12540
  setExitRequested(true);
12224
12541
  setTimeout(() => {
@@ -12407,7 +12724,9 @@ function CliApp() {
12407
12724
  subagentConfigs
12408
12725
  });
12409
12726
  const subagentDelegateTool = createSubagentDelegateTool(orchestrator, newSession.id);
12410
- const allTools = [...b4mTools, ...mcpTools, subagentDelegateTool];
12727
+ const todoStore = createTodoStore();
12728
+ const writeTodosTool = createWriteTodosTool(todoStore);
12729
+ const allTools = [...b4mTools, ...mcpTools, subagentDelegateTool, writeTodosTool];
12411
12730
  console.log(`\u{1F916} Subagent delegation enabled (explore, plan, review)`);
12412
12731
  const projectDir = state.configStore.getProjectConfigDir();
12413
12732
  const contextResult = await loadContextFiles(projectDir);
@@ -12427,46 +12746,7 @@ function CliApp() {
12427
12746
  Follow these project-specific instructions:
12428
12747
 
12429
12748
  ${contextResult.mergedContent}` : "";
12430
- const cliSystemPrompt = `You are an autonomous AI assistant with access to tools. Your job is to help users by taking action and solving problems proactively.
12431
-
12432
- CORE BEHAVIOR:
12433
- - Be proactive: Take action instead of asking for permission or clarification
12434
- - Make smart assumptions: If unclear, choose the most reasonable interpretation
12435
- - Use conversation history: Reference previous exchanges to understand context
12436
- - Complete tasks fully: Don't just show what to do - actually do it
12437
-
12438
- FOR CODING TASKS:
12439
- - You have file system access (file_read, create_file, glob_files, grep_search, delete_file)
12440
- - When user says "enhance", "update", "fix", "improve" a file:
12441
- * Use file_read to get current content
12442
- * Generate the improved version
12443
- * Use create_file to write it back
12444
- * Don't just show suggestions - make the changes
12445
- - When searching for code: Use grep_search or glob_files to find relevant files
12446
- - Permission system will ask for approval before any file writes
12447
-
12448
- SUBAGENT DELEGATION:
12449
- - You have access to specialized subagents via the subagent_delegate tool
12450
- - Use subagents for focused exploration, planning, or review tasks:
12451
- * explore: Fast read-only codebase search (e.g., "find all auth files", "locate API endpoints")
12452
- * plan: Break down complex tasks into actionable steps
12453
- * review: Analyze code quality and identify issues
12454
- - Subagents keep the main conversation clean and run faster with optimized models
12455
- - Delegate when you need to search extensively or analyze code structure
12456
-
12457
- FOR GENERAL TASKS:
12458
- - Use available tools to get information (weather, web search, calculations, etc.)
12459
- - When user asks follow-up questions, use conversation context to understand what they're referring to
12460
- - If user asks "how about X?" after a previous question, apply the same question type to X
12461
-
12462
- EXAMPLES:
12463
- - "what should I wear in Texas?" \u2192 use weather tool for Texas
12464
- - "how about Japan?" \u2192 use weather tool for Japan (applying same question from context)
12465
- - "enhance README" \u2192 file_read \u2192 generate \u2192 create_file
12466
- - "what packages installed?" \u2192 glob_files "**/package.json" \u2192 file_read
12467
- - "find all components using React hooks" \u2192 subagent_delegate(task="find all components using React hooks", type="explore")
12468
-
12469
- Remember: Use context from previous messages to understand follow-up questions.${contextSection}`;
12749
+ const cliSystemPrompt = buildCoreSystemPrompt(contextSection);
12470
12750
  const maxIterations = config.preferences.maxIterations === null ? 999999 : config.preferences.maxIterations;
12471
12751
  const agent = new ReActAgent({
12472
12752
  userId: config.userId,
@@ -12486,38 +12766,27 @@ Remember: Use context from previous messages to understand follow-up questions.$
12486
12766
  const lastIdx = pendingMessages.length - 1;
12487
12767
  if (lastIdx >= 0 && pendingMessages[lastIdx].role === "assistant") {
12488
12768
  const existingSteps = pendingMessages[lastIdx].metadata?.steps || [];
12489
- const MAX_INPUT_LENGTH = 500;
12490
- let truncatedStep = step;
12491
- if (step.type === "action" && step.metadata?.toolInput) {
12492
- const inputStr = typeof step.metadata.toolInput === "string" ? step.metadata.toolInput : JSON.stringify(step.metadata.toolInput);
12493
- if (inputStr.length > MAX_INPUT_LENGTH) {
12494
- const truncatedInput = inputStr.slice(0, MAX_INPUT_LENGTH) + `... (${inputStr.length - MAX_INPUT_LENGTH} more chars)`;
12495
- truncatedStep = {
12496
- ...step,
12497
- metadata: {
12498
- ...step.metadata,
12499
- toolInput: truncatedInput
12500
- }
12501
- };
12502
- }
12503
- }
12769
+ const formattedStep = formatStep(step);
12504
12770
  updatePendingMessage(lastIdx, {
12505
12771
  ...pendingMessages[lastIdx],
12506
12772
  metadata: {
12507
12773
  ...pendingMessages[lastIdx].metadata,
12508
- steps: [...existingSteps, truncatedStep]
12774
+ steps: [...existingSteps, formattedStep]
12509
12775
  }
12510
12776
  });
12511
12777
  }
12512
12778
  };
12513
12779
  agent.on("thought", stepHandler);
12780
+ agent.on("observation", stepHandler);
12514
12781
  agent.on("action", stepHandler);
12515
12782
  orchestrator.setBeforeRunCallback((subagent, _subagentType) => {
12516
12783
  subagent.on("thought", stepHandler);
12784
+ subagent.on("observation", stepHandler);
12517
12785
  subagent.on("action", stepHandler);
12518
12786
  });
12519
12787
  orchestrator.setAfterRunCallback((subagent, _subagentType) => {
12520
12788
  subagent.off("thought", stepHandler);
12789
+ subagent.off("observation", stepHandler);
12521
12790
  subagent.off("action", stepHandler);
12522
12791
  });
12523
12792
  setState((prev) => ({
@@ -12564,6 +12833,8 @@ Remember: Use context from previous messages to understand follow-up questions.$
12564
12833
  return;
12565
12834
  }
12566
12835
  useCliStore.getState().setIsThinking(true);
12836
+ const abortController = new AbortController();
12837
+ setState((prev) => ({ ...prev, abortController }));
12567
12838
  const currentSteps = [];
12568
12839
  const stepHandler = (step) => {
12569
12840
  currentSteps.push(step);
@@ -12623,7 +12894,8 @@ Remember: Use context from previous messages to understand follow-up questions.$
12623
12894
  content: msg.content
12624
12895
  }));
12625
12896
  const result = await state.agent.run(messageContent, {
12626
- previousMessages: previousMessages.length > 0 ? previousMessages : void 0
12897
+ previousMessages: previousMessages.length > 0 ? previousMessages : void 0,
12898
+ signal: abortController.signal
12627
12899
  });
12628
12900
  const permissionDenied = result.finalAnswer.startsWith("Permission denied for tool");
12629
12901
  if (permissionDenied) {
@@ -12641,7 +12913,7 @@ Remember: Use context from previous messages to understand follow-up questions.$
12641
12913
  content: result.finalAnswer,
12642
12914
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
12643
12915
  metadata: {
12644
- steps: result.steps,
12916
+ steps: result.steps.map(formatStep),
12645
12917
  tokenUsage: {
12646
12918
  prompt: 0,
12647
12919
  completion: 0,
@@ -12668,6 +12940,33 @@ Remember: Use context from previous messages to understand follow-up questions.$
12668
12940
  useCliStore.getState().setIsThinking(false);
12669
12941
  } catch (error) {
12670
12942
  useCliStore.getState().setIsThinking(false);
12943
+ if (error instanceof Error && error.name === "AbortError") {
12944
+ logger.debug("[ABORT] Custom command aborted by user");
12945
+ const currentSession2 = useCliStore.getState().session;
12946
+ if (currentSession2) {
12947
+ const messages2 = [...currentSession2.messages];
12948
+ const lastMessage2 = messages2[messages2.length - 1];
12949
+ if (lastMessage2 && lastMessage2.role === "assistant") {
12950
+ messages2[messages2.length - 1] = {
12951
+ ...lastMessage2,
12952
+ content: "\u26A0\uFE0F Operation cancelled by user",
12953
+ metadata: {
12954
+ ...lastMessage2.metadata,
12955
+ cancelled: true
12956
+ }
12957
+ };
12958
+ }
12959
+ const sessionWithCancel = {
12960
+ ...currentSession2,
12961
+ messages: messages2,
12962
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
12963
+ };
12964
+ setState((prev) => ({ ...prev, session: sessionWithCancel }));
12965
+ setStoreSession(sessionWithCancel);
12966
+ await state.sessionStore.save(sessionWithCancel);
12967
+ }
12968
+ return;
12969
+ }
12671
12970
  if (error?.message?.includes("Permission denied")) {
12672
12971
  console.log("\n\u26A0\uFE0F Action blocked by permission settings");
12673
12972
  const currentSession2 = useCliStore.getState().session;
@@ -12708,6 +13007,7 @@ Remember: Use context from previous messages to understand follow-up questions.$
12708
13007
  setState((prev) => ({ ...prev, session: sessionWithError }));
12709
13008
  setStoreSession(sessionWithError);
12710
13009
  } finally {
13010
+ setState((prev) => ({ ...prev, abortController: null }));
12711
13011
  state.agent.off("thought", stepHandler);
12712
13012
  state.agent.off("action", stepHandler);
12713
13013
  }
@@ -12727,6 +13027,8 @@ Remember: Use context from previous messages to understand follow-up questions.$
12727
13027
  return;
12728
13028
  }
12729
13029
  useCliStore.getState().setIsThinking(true);
13030
+ const abortController = new AbortController();
13031
+ setState((prev) => ({ ...prev, abortController }));
12730
13032
  try {
12731
13033
  let messageContent = message;
12732
13034
  let userMessageContent = message;
@@ -12764,7 +13066,8 @@ Remember: Use context from previous messages to understand follow-up questions.$
12764
13066
  content: msg.content
12765
13067
  }));
12766
13068
  const result = await state.agent.run(messageContent, {
12767
- previousMessages: previousMessages.length > 0 ? previousMessages : void 0
13069
+ previousMessages: previousMessages.length > 0 ? previousMessages : void 0,
13070
+ signal: abortController.signal
12768
13071
  });
12769
13072
  const permissionDenied = result.finalAnswer.startsWith("Permission denied for tool");
12770
13073
  if (permissionDenied) {
@@ -12783,7 +13086,7 @@ Remember: Use context from previous messages to understand follow-up questions.$
12783
13086
  completion: 0,
12784
13087
  total: result.completionInfo.totalTokens
12785
13088
  },
12786
- steps: result.steps,
13089
+ steps: result.steps.map(formatStep),
12787
13090
  // Complete history: thoughts, actions, observations
12788
13091
  permissionDenied
12789
13092
  }
@@ -12804,6 +13107,30 @@ Remember: Use context from previous messages to understand follow-up questions.$
12804
13107
  await state.sessionStore.save(updatedSession);
12805
13108
  } catch (error) {
12806
13109
  useCliStore.getState().clearPendingMessages();
13110
+ if (error instanceof Error && error.name === "AbortError") {
13111
+ logger.debug("[ABORT] Operation aborted by user");
13112
+ const currentSession = useCliStore.getState().session;
13113
+ if (currentSession) {
13114
+ const cancelMessage = {
13115
+ id: uuidv410(),
13116
+ role: "assistant",
13117
+ content: "\u26A0\uFE0F Operation cancelled by user",
13118
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
13119
+ metadata: {
13120
+ cancelled: true
13121
+ }
13122
+ };
13123
+ const sessionWithCancel = {
13124
+ ...currentSession,
13125
+ messages: [...currentSession.messages, cancelMessage],
13126
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
13127
+ };
13128
+ setState((prev) => ({ ...prev, session: sessionWithCancel }));
13129
+ setStoreSession(sessionWithCancel);
13130
+ await state.sessionStore.save(sessionWithCancel);
13131
+ }
13132
+ return;
13133
+ }
12807
13134
  if (error instanceof Error) {
12808
13135
  if (error.message.includes("Authentication failed") || error.message.includes("Authentication expired")) {
12809
13136
  console.log("\n\u274C Authentication failed");
@@ -12812,6 +13139,9 @@ Remember: Use context from previous messages to understand follow-up questions.$
12812
13139
  }
12813
13140
  }
12814
13141
  console.error("Error processing message:", error);
13142
+ } finally {
13143
+ setState((prev) => ({ ...prev, abortController: null }));
13144
+ useCliStore.getState().setIsThinking(false);
12815
13145
  }
12816
13146
  };
12817
13147
  const handleBashCommand = useCallback(
@@ -12862,7 +13192,7 @@ ${output}` : output.trim() || "(no output)",
12862
13192
  if (!imageStore) {
12863
13193
  if (!imageStoreInitPromise.current) {
12864
13194
  imageStoreInitPromise.current = (async () => {
12865
- const { ImageStore: ImageStoreClass } = await import("./ImageStore-RQZ7OF7P.js");
13195
+ const { ImageStore: ImageStoreClass } = await import("./ImageStore-MMUOUPI2.js");
12866
13196
  const newImageStore = new ImageStoreClass();
12867
13197
  setState((prev) => ({
12868
13198
  ...prev,