@bike4mind/cli 0.2.11 → 0.2.12-cli-todo-list.17408

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("~")) {
@@ -769,6 +774,16 @@ var ImageInputDetector = class {
769
774
  };
770
775
 
771
776
  // src/components/InputPrompt.tsx
777
+ function looksLikeFilePath(input) {
778
+ const trimmed = input.trim();
779
+ if (trimmed.startsWith("/")) {
780
+ const hasMultipleSegments = trimmed.indexOf("/", 1) !== -1;
781
+ if (hasMultipleSegments) return true;
782
+ }
783
+ if (trimmed.startsWith("~")) return true;
784
+ if (trimmed.match(/^[a-zA-Z]:\\/)) return true;
785
+ return false;
786
+ }
772
787
  function findAtTrigger(value) {
773
788
  for (let i = value.length - 1; i >= 0; i--) {
774
789
  if (value[i] === "@") {
@@ -812,7 +827,7 @@ function InputPrompt({
812
827
  useEffect(() => {
813
828
  onBashModeChange?.(isBashMode);
814
829
  }, [isBashMode, onBashModeChange]);
815
- const shouldShowCommandAutocomplete = value.startsWith("/") && !disabled && !fileAutocomplete?.active;
830
+ const shouldShowCommandAutocomplete = value.startsWith("/") && !disabled && !fileAutocomplete?.active && !looksLikeFilePath(value);
816
831
  const commandQuery = shouldShowCommandAutocomplete ? value.slice(1) : "";
817
832
  const filteredCommands = useMemo(() => {
818
833
  if (!shouldShowCommandAutocomplete) return [];
@@ -1358,7 +1373,7 @@ function isNameSuffix(value) {
1358
1373
 
1359
1374
  // src/utils/processFileReferences.ts
1360
1375
  var FILE_REFERENCE_REGEX = /(?:^|\s)@([^\s@]+)/g;
1361
- function looksLikeFilePath(ref) {
1376
+ function looksLikeFilePath2(ref) {
1362
1377
  if (ref.includes("/") || ref.includes(path2.sep)) {
1363
1378
  return true;
1364
1379
  }
@@ -1381,7 +1396,7 @@ function extractFileReferences(message) {
1381
1396
  let match;
1382
1397
  while ((match = FILE_REFERENCE_REGEX.exec(message)) !== null) {
1383
1398
  const ref = match[1];
1384
- if (looksLikeFilePath(ref)) {
1399
+ if (looksLikeFilePath2(ref)) {
1385
1400
  references.push(ref);
1386
1401
  }
1387
1402
  }
@@ -3093,6 +3108,21 @@ ${options.context}` : this.getSystemPrompt()
3093
3108
  let finalAnswer = "";
3094
3109
  let reachedMaxIterations = false;
3095
3110
  while (iterations < maxIterations) {
3111
+ if (options.signal?.aborted) {
3112
+ this.context.logger.info("[ReActAgent] Operation aborted by user");
3113
+ const result2 = {
3114
+ finalAnswer: "Interrupted",
3115
+ steps: this.steps,
3116
+ completionInfo: {
3117
+ totalTokens: this.totalTokens,
3118
+ iterations,
3119
+ toolCalls: this.toolCallCount,
3120
+ reachedMaxIterations: false
3121
+ }
3122
+ };
3123
+ this.emit("complete", result2);
3124
+ return result2;
3125
+ }
3096
3126
  iterations++;
3097
3127
  this.context.logger.debug(`[ReActAgent] Starting iteration ${iterations}/${maxIterations}`);
3098
3128
  let iterationComplete = false;
@@ -3106,7 +3136,8 @@ ${options.context}` : this.getSystemPrompt()
3106
3136
  stream: false,
3107
3137
  tools: this.context.tools,
3108
3138
  maxTokens,
3109
- temperature
3139
+ temperature,
3140
+ abortSignal: options.signal
3110
3141
  },
3111
3142
  async (texts, completionInfo) => {
3112
3143
  for (const text of texts) {
@@ -3249,6 +3280,9 @@ ${options.context}` : this.getSystemPrompt()
3249
3280
  this.emit("final_answer", finalStep);
3250
3281
  }
3251
3282
  }
3283
+ if (options.signal?.aborted && !finalAnswer) {
3284
+ finalAnswer = "Interrupted";
3285
+ }
3252
3286
  const result = {
3253
3287
  finalAnswer,
3254
3288
  steps: this.steps,
@@ -3356,6 +3390,70 @@ Remember: You are an autonomous AGENT. Act independently and solve problems proa
3356
3390
  }
3357
3391
  };
3358
3392
 
3393
+ // src/core/prompts.ts
3394
+ var TOOL_GREP_SEARCH = "grep_search";
3395
+ var TOOL_GLOB_FILES = "glob_files";
3396
+ var TOOL_FILE_READ = "file_read";
3397
+ var TOOL_EDIT_LOCAL_FILE = "edit_local_file";
3398
+ var TOOL_CREATE_FILE = "create_file";
3399
+ var TOOL_BASH_EXECUTE = "bash_execute";
3400
+ var TOOL_SUBAGENT_DELEGATE = "subagent_delegate";
3401
+ var TOOL_WRITE_TODOS = "write_todos";
3402
+ var EXPLORE_SUBAGENT_TYPE = "explore";
3403
+ function buildCoreSystemPrompt(contextSection = "") {
3404
+ return `You are an autonomous AI assistant with access to tools. Your job is to help users by taking action and solving problems proactively.
3405
+
3406
+ CORE BEHAVIOR:
3407
+ - Be proactive: Take action instead of asking for permission or clarification
3408
+ - Make smart assumptions: If unclear, choose the most reasonable interpretation
3409
+ - Use conversation history: Reference previous exchanges to understand context
3410
+ - Complete tasks fully: Don't just show what to do - actually do it
3411
+
3412
+ FOR SOFTWARE ENGINEERING TASKS:
3413
+ When requested to perform tasks like fixing bugs, adding features, refactoring, or explaining code, follow this sequence:
3414
+ 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}'.
3415
+ 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.
3416
+ 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.
3417
+ 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.
3418
+ 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.
3419
+ 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.
3420
+ 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.
3421
+ 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.
3422
+
3423
+ SUBAGENT DELEGATION:
3424
+ - You have access to specialized subagents via the ${TOOL_SUBAGENT_DELEGATE} tool
3425
+ - Use subagents for focused exploration, planning, or review tasks:
3426
+ * explore: Fast read-only codebase search (e.g., "find all auth files", "locate API endpoints")
3427
+ * plan: Break down complex tasks into actionable steps
3428
+ * review: Analyze code quality and identify issues
3429
+ - Subagents keep the main conversation clean and run faster with optimized models
3430
+ - Delegate when you need to search extensively or analyze code structure
3431
+
3432
+ FOR GENERAL TASKS:
3433
+ - Use available tools to get information (weather, web search, calculations, etc.)
3434
+ - When user asks follow-up questions, use conversation context to understand what they're referring to
3435
+ - If user asks "how about X?" after a previous question, apply the same question type to X
3436
+
3437
+ ## Shell tool output token efficiency:
3438
+ IT IS CRITICAL TO FOLLOW THESE GUIDELINES TO AVOID EXCESSIVE TOKEN CONSUMPTION.
3439
+
3440
+ - Always prefer command flags that reduce output verbosity when using '${TOOL_BASH_EXECUTE}'.
3441
+ - Aim to minimize tool output tokens while still capturing necessary information.
3442
+ - If a command is expected to produce a lot of output, use quiet or silent flags where available and appropriate.
3443
+ - 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.
3444
+ - 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'.
3445
+ - 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.
3446
+
3447
+ EXAMPLES:
3448
+ - "what should I wear in Texas?" \u2192 use weather tool for Texas
3449
+ - "how about Japan?" \u2192 use weather tool for Japan (applying same question from context)
3450
+ - "enhance README" \u2192 ${TOOL_FILE_READ} \u2192 generate \u2192 ${TOOL_EDIT_LOCAL_FILE}
3451
+ - "what packages installed?" \u2192 ${TOOL_GLOB_FILES} "**/package.json" \u2192 ${TOOL_FILE_READ}
3452
+ - "find all components using React hooks" \u2192 ${TOOL_SUBAGENT_DELEGATE}(task="find all components using React hooks", type="explore")
3453
+
3454
+ Remember: Use context from previous messages to understand follow-up questions.${contextSection}`;
3455
+ }
3456
+
3359
3457
  // ../../b4m-core/packages/services/dist/src/referService/generateCodes.js
3360
3458
  import { randomBytes } from "crypto";
3361
3459
  import range from "lodash/range.js";
@@ -8707,98 +8805,80 @@ var createFileTool = {
8707
8805
  };
8708
8806
 
8709
8807
  // ../../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";
8808
+ import { glob } from "glob";
8809
+ import { stat } from "fs/promises";
8712
8810
  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;
8811
+ var DEFAULT_IGNORE_PATTERNS2 = [
8812
+ "**/node_modules/**",
8813
+ "**/.git/**",
8814
+ "**/dist/**",
8815
+ "**/build/**",
8816
+ "**/.next/**",
8817
+ "**/coverage/**",
8818
+ "**/.turbo/**",
8819
+ "**/.sst/**",
8820
+ "**/*.min.js",
8821
+ "**/*.min.css"
8822
+ ];
8823
+ async function findFiles(params) {
8824
+ const { pattern, dir_path, case_sensitive = true, respect_git_ignore = true } = params;
8746
8825
  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);
8826
+ const targetDir = dir_path ? path9.resolve(baseCwd, path9.normalize(dir_path)) : baseCwd;
8827
+ if (!targetDir.startsWith(baseCwd)) {
8828
+ throw new Error(`Access denied: Cannot search outside of current working directory`);
8829
+ }
8830
+ const ignorePatterns = respect_git_ignore ? DEFAULT_IGNORE_PATTERNS2 : [];
8831
+ const matches = await glob(pattern, {
8832
+ cwd: targetDir,
8833
+ dot: false,
8834
+ // Don't match hidden files by default
8835
+ ignore: ignorePatterns,
8836
+ absolute: true,
8837
+ nodir: true,
8838
+ // Only return files, not directories
8839
+ nocase: !case_sensitive,
8840
+ // Case-insensitive if requested
8841
+ maxDepth: 20
8842
+ // Reasonable depth limit
8843
+ });
8752
8844
  if (matches.length === 0) {
8753
- return `No files found matching pattern: ${pattern}`;
8845
+ return `No files found matching pattern: ${pattern}${dir_path ? ` in ${dir_path}` : ""}`;
8754
8846
  }
8755
- const filesInfo = [];
8756
- for (const match of matches) {
8847
+ const filesWithStats = [];
8848
+ for (const filePath of matches) {
8757
8849
  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()
8850
+ const stats = await stat(filePath);
8851
+ filesWithStats.push({
8852
+ path: filePath,
8853
+ mtime: stats.mtimeMs
8765
8854
  });
8766
- } catch (error) {
8855
+ } catch {
8767
8856
  continue;
8768
8857
  }
8769
8858
  }
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}
8859
+ filesWithStats.sort((a, b) => b.mtime - a.mtime);
8860
+ const MAX_RESULTS = 500;
8861
+ const truncated = filesWithStats.length > MAX_RESULTS;
8862
+ const results = truncated ? filesWithStats.slice(0, MAX_RESULTS) : filesWithStats;
8863
+ const summary = `Found ${filesWithStats.length} file(s)${truncated ? ` (showing first ${MAX_RESULTS})` : ""} matching: ${pattern}`;
8864
+ const dirInfo = dir_path ? `
8865
+ Directory: ${dir_path}` : "";
8866
+ const filesList = results.map((file) => path9.relative(baseCwd, file.path)).join("\n");
8867
+ return `${summary}${dirInfo}
8868
+
8781
8869
  ${filesList}`;
8782
8870
  }
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
8871
  var globFilesTool = {
8792
8872
  name: "glob_files",
8793
8873
  implementation: (context) => ({
8794
8874
  toolFn: async (value) => {
8795
8875
  const params = value;
8796
- context.logger.info("\u{1F50D} GlobFiles: Listing files", {
8876
+ context.logger.info("\u{1F50D} GlobFiles: Finding files", {
8797
8877
  pattern: params.pattern,
8798
- cwd: params.cwd || "."
8878
+ dir_path: params.dir_path || "."
8799
8879
  });
8800
8880
  try {
8801
- const result = await listFiles(params);
8881
+ const result = await findFiles(params);
8802
8882
  context.logger.info("\u2705 GlobFiles: Success");
8803
8883
  return result;
8804
8884
  } catch (error) {
@@ -8808,25 +8888,25 @@ var globFilesTool = {
8808
8888
  },
8809
8889
  toolSchema: {
8810
8890
  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.",
8891
+ 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
8892
  parameters: {
8813
8893
  type: "object",
8814
8894
  properties: {
8815
8895
  pattern: {
8816
8896
  type: "string",
8817
- description: 'Glob pattern to match files (e.g., "*.ts", "src/**/*.tsx", "test/*.{js,ts}"). Use ** for recursive directory search.'
8897
+ 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
8898
  },
8819
- cwd: {
8899
+ dir_path: {
8820
8900
  type: "string",
8821
- description: "Directory to search in, relative to current working directory (optional). Defaults to current directory."
8901
+ 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
8902
  },
8823
- includeHidden: {
8903
+ case_sensitive: {
8824
8904
  type: "boolean",
8825
- description: "Include hidden files and directories (starting with .). Defaults to false."
8905
+ 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
8906
  },
8827
- maxResults: {
8828
- type: "number",
8829
- description: "Maximum number of results to return (default: 1000). Prevents overwhelming output."
8907
+ respect_git_ignore: {
8908
+ type: "boolean",
8909
+ 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
8910
  }
8831
8911
  },
8832
8912
  required: ["pattern"]
@@ -8836,158 +8916,144 @@ var globFilesTool = {
8836
8916
  };
8837
8917
 
8838
8918
  // ../../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";
8919
+ import { globStream } from "glob";
8920
+ import { readFile, stat as stat2 } from "fs/promises";
8841
8921
  import path10 from "path";
8842
8922
  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 = [];
8923
+ var DEFAULT_IGNORE_PATTERNS3 = [
8924
+ "**/node_modules/**",
8925
+ "**/.git/**",
8926
+ "**/dist/**",
8927
+ "**/build/**",
8928
+ "**/.next/**",
8929
+ "**/coverage/**",
8930
+ "**/*.min.js",
8931
+ "**/*.min.css",
8932
+ "**/package-lock.json",
8933
+ "**/pnpm-lock.yaml",
8934
+ "**/yarn.lock"
8935
+ ];
8936
+ async function isBinaryFile2(filePath) {
8850
8937
  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) {
8938
+ const { openSync, readSync, closeSync } = await import("fs");
8939
+ const buffer = Buffer.alloc(8192);
8940
+ const fd = openSync(filePath, "r");
8941
+ const bytesRead = readSync(fd, buffer, 0, 8192, 0);
8942
+ closeSync(fd);
8943
+ const chunk = buffer.slice(0, bytesRead);
8944
+ return chunk.includes(0);
8945
+ } catch {
8946
+ return true;
8870
8947
  }
8871
- return matches;
8948
+ }
8949
+ function isPathWithinWorkspace(targetPath, baseCwd) {
8950
+ const resolvedTarget = path10.resolve(targetPath);
8951
+ const resolvedBase = path10.resolve(baseCwd);
8952
+ return resolvedTarget.startsWith(resolvedBase);
8872
8953
  }
8873
8954
  async function searchFiles2(params) {
8874
- const { pattern, filePattern = "**/*", cwd: relativeCwd, caseSensitive = false, maxResults = 100, contextLines = 0 } = params;
8955
+ const { pattern, dir_path, include } = params;
8875
8956
  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`);
8957
+ const targetDir = dir_path ? path10.resolve(baseCwd, dir_path) : baseCwd;
8958
+ if (!isPathWithinWorkspace(targetDir, baseCwd)) {
8959
+ throw new Error(`Path validation failed: "${dir_path}" resolves outside the allowed workspace directory`);
8960
+ }
8961
+ try {
8962
+ const stats = await stat2(targetDir);
8963
+ if (!stats.isDirectory()) {
8964
+ throw new Error(`Path is not a directory: ${dir_path}`);
8965
+ }
8966
+ } catch (error) {
8967
+ if (error.code === "ENOENT") {
8968
+ throw new Error(`Path does not exist: ${dir_path}`);
8969
+ }
8970
+ throw error;
8879
8971
  }
8880
8972
  let searchRegex;
8881
8973
  try {
8882
- const flags = caseSensitive ? "g" : "gi";
8883
- searchRegex = new RegExp(pattern, flags);
8974
+ searchRegex = new RegExp(pattern, "i");
8884
8975
  } catch (error) {
8885
8976
  throw new Error(`Invalid regex pattern: ${pattern}`);
8886
8977
  }
8887
- const filesToSearch = await walkDir2(targetCwd, filePattern, 1e4);
8888
- const filteredFiles = [];
8889
- for (const match of filesToSearch) {
8890
- const fullPath = path10.join(targetCwd, match);
8978
+ const globPattern = include || "**/*";
8979
+ const filesStream = globStream(globPattern, {
8980
+ cwd: targetDir,
8981
+ dot: false,
8982
+ // Skip hidden files
8983
+ ignore: DEFAULT_IGNORE_PATTERNS3,
8984
+ absolute: true,
8985
+ nodir: true
8986
+ // Only match files, not directories
8987
+ });
8988
+ const allMatches = [];
8989
+ let filesSearched = 0;
8990
+ const maxMatches = 500;
8991
+ for await (const filePath of filesStream) {
8992
+ if (allMatches.length >= maxMatches) {
8993
+ break;
8994
+ }
8995
+ const fileAbsPath = filePath;
8891
8996
  try {
8892
- const stats = statSync6(fullPath);
8893
- if (stats.isDirectory() || stats.size > MAX_FILE_SIZE3) {
8997
+ const stats = await stat2(fileAbsPath);
8998
+ if (stats.size > MAX_FILE_SIZE3) {
8894
8999
  continue;
8895
9000
  }
8896
- if (isBinaryFile2(fullPath)) {
9001
+ if (await isBinaryFile2(fileAbsPath)) {
8897
9002
  continue;
8898
9003
  }
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);
9004
+ filesSearched++;
9005
+ const content = await readFile(fileAbsPath, "utf8");
9006
+ const lines = content.split(/\r?\n/);
9007
+ lines.forEach((line, index) => {
9008
+ if (allMatches.length < maxMatches && searchRegex.test(line)) {
9009
+ allMatches.push({
9010
+ filePath: path10.relative(targetDir, fileAbsPath) || path10.basename(fileAbsPath),
9011
+ lineNumber: index + 1,
9012
+ line
9013
+ });
8942
9014
  }
8943
- searchRegex.lastIndex = 0;
9015
+ });
9016
+ } catch (readError) {
9017
+ const errCode = readError.code;
9018
+ if (errCode !== "ENOENT" && errCode !== "EACCES") {
9019
+ console.debug(`Could not read ${fileAbsPath}: ${readError.message}`);
8944
9020
  }
8945
- } catch {
8946
- continue;
8947
9021
  }
8948
9022
  }
8949
- if (matches.length === 0) {
8950
- return `No matches found for pattern: ${pattern}
8951
- Searched ${filesSearched} file(s)`;
9023
+ const searchDirDisplay = dir_path || ".";
9024
+ const filterInfo = include ? ` (filter: "${include}")` : "";
9025
+ if (allMatches.length === 0) {
9026
+ return `No matches found for pattern "${pattern}" in "${searchDirDisplay}"${filterInfo}.
9027
+ Searched ${filesSearched} file(s).`;
8952
9028
  }
8953
- const truncated = matches.length >= maxResults;
8954
- const summary = `Found ${matches.length} match(es)${truncated ? ` (truncated to ${maxResults})` : ""} in ${filesSearched} file(s)
9029
+ const matchesByFile = allMatches.reduce((acc, match) => {
9030
+ if (!acc[match.filePath]) {
9031
+ acc[match.filePath] = [];
9032
+ }
9033
+ acc[match.filePath].push(match);
9034
+ acc[match.filePath].sort((a, b) => a.lineNumber - b.lineNumber);
9035
+ return acc;
9036
+ }, {});
9037
+ const matchCount = allMatches.length;
9038
+ const matchTerm = matchCount === 1 ? "match" : "matches";
9039
+ const truncated = matchCount >= maxMatches;
9040
+ let result = `Found ${matchCount} ${matchTerm}${truncated ? " (truncated)" : ""} for pattern "${pattern}" in "${searchDirDisplay}"${filterInfo}:
8955
9041
  `;
8956
- const patternInfo = `Pattern: ${pattern}${caseSensitive ? "" : " (case-insensitive)"}
9042
+ result += `Searched ${filesSearched} file(s)
9043
+ ---
8957
9044
  `;
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;
9045
+ for (const filePath in matchesByFile) {
9046
+ result += `File: ${filePath}
9047
+ `;
9048
+ matchesByFile[filePath].forEach((match) => {
9049
+ const trimmedLine = match.line.trim();
9050
+ const displayLine = trimmedLine.length > 200 ? trimmedLine.slice(0, 200) + "..." : trimmedLine;
9051
+ result += `L${match.lineNumber}: ${displayLine}
9052
+ `;
9053
+ });
9054
+ result += "---\n";
8990
9055
  }
9056
+ return result.trim();
8991
9057
  }
8992
9058
  var grepSearchTool = {
8993
9059
  name: "grep_search",
@@ -8996,8 +9062,8 @@ var grepSearchTool = {
8996
9062
  const params = value;
8997
9063
  context.logger.info("\u{1F50D} GrepSearch: Searching files", {
8998
9064
  pattern: params.pattern,
8999
- filePattern: params.filePattern || "**/*",
9000
- cwd: params.cwd || "."
9065
+ dir_path: params.dir_path || ".",
9066
+ include: params.include || "**/*"
9001
9067
  });
9002
9068
  try {
9003
9069
  const result = await searchFiles2(params);
@@ -9010,33 +9076,21 @@ var grepSearchTool = {
9010
9076
  },
9011
9077
  toolSchema: {
9012
9078
  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.",
9079
+ 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
9080
  parameters: {
9015
9081
  type: "object",
9016
9082
  properties: {
9017
9083
  pattern: {
9018
9084
  type: "string",
9019
- description: "Regular expression pattern to search for in file contents"
9085
+ description: "The regular expression (regex) pattern to search for within file contents (e.g., 'function\\s+myFunction', 'import\\s+\\{.*\\}\\s+from\\s+.*')."
9020
9086
  },
9021
- filePattern: {
9087
+ dir_path: {
9022
9088
  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.'
9089
+ description: "Optional: The absolute path to the directory to search within. If omitted, searches the current working directory."
9024
9090
  },
9025
- cwd: {
9091
+ include: {
9026
9092
  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."
9093
+ 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
9094
  }
9041
9095
  },
9042
9096
  required: ["pattern"]
@@ -9047,7 +9101,7 @@ var grepSearchTool = {
9047
9101
 
9048
9102
  // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/deleteFile/index.js
9049
9103
  import { promises as fs10 } from "fs";
9050
- import { existsSync as existsSync6, statSync as statSync7 } from "fs";
9104
+ import { existsSync as existsSync6, statSync as statSync5 } from "fs";
9051
9105
  import path11 from "path";
9052
9106
  async function deleteFile(params) {
9053
9107
  const { path: filePath, recursive = false } = params;
@@ -9060,7 +9114,7 @@ async function deleteFile(params) {
9060
9114
  if (!existsSync6(resolvedPath)) {
9061
9115
  throw new Error(`File or directory not found: ${filePath}`);
9062
9116
  }
9063
- const stats = statSync7(resolvedPath);
9117
+ const stats = statSync5(resolvedPath);
9064
9118
  const isDirectory = stats.isDirectory();
9065
9119
  const size = stats.size;
9066
9120
  if (isDirectory && !recursive) {
@@ -9081,7 +9135,7 @@ var deleteFileTool = {
9081
9135
  toolFn: async (value) => {
9082
9136
  const params = value;
9083
9137
  const resolvedPath = path11.resolve(process.cwd(), path11.normalize(params.path));
9084
- const isDirectory = existsSync6(resolvedPath) && statSync7(resolvedPath).isDirectory();
9138
+ const isDirectory = existsSync6(resolvedPath) && statSync5(resolvedPath).isDirectory();
9085
9139
  context.logger.info(`\u{1F5D1}\uFE0F DeleteFile: Deleting ${isDirectory ? "directory" : "file"}`, {
9086
9140
  path: params.path,
9087
9141
  recursive: params.recursive
@@ -9857,7 +9911,7 @@ var ToolErrorType;
9857
9911
 
9858
9912
  // src/utils/diffPreview.ts
9859
9913
  import * as Diff from "diff";
9860
- import { readFile } from "fs/promises";
9914
+ import { readFile as readFile2 } from "fs/promises";
9861
9915
  import { existsSync as existsSync8 } from "fs";
9862
9916
  async function generateFileDiffPreview(args) {
9863
9917
  try {
@@ -9872,7 +9926,7 @@ ${preview}${hasMore ? `
9872
9926
 
9873
9927
  ... (${lines2.length - 20} more lines)` : ""}`;
9874
9928
  }
9875
- const currentContent = await readFile(args.path, "utf-8");
9929
+ const currentContent = await readFile2(args.path, "utf-8");
9876
9930
  const patch = Diff.createPatch(
9877
9931
  args.path,
9878
9932
  currentContent,
@@ -11248,6 +11302,10 @@ var ServerLlmBackend = class {
11248
11302
  */
11249
11303
  async complete(model, messages, options, callback) {
11250
11304
  logger.debug(`[ServerLlmBackend] Starting complete() with model: ${model}`);
11305
+ if (options.abortSignal?.aborted) {
11306
+ logger.debug("[ServerLlmBackend] Request aborted before start");
11307
+ return;
11308
+ }
11251
11309
  return new Promise(async (resolve3, reject) => {
11252
11310
  try {
11253
11311
  logger.debug("[ServerLlmBackend] Making streaming request...");
@@ -11348,7 +11406,25 @@ var ServerLlmBackend = class {
11348
11406
  }
11349
11407
  }
11350
11408
  });
11409
+ if (options.abortSignal) {
11410
+ const abortHandler = () => {
11411
+ logger.debug("[ServerLlmBackend] Abort signal received, destroying stream");
11412
+ response.data.destroy();
11413
+ resolve3();
11414
+ };
11415
+ if (options.abortSignal.aborted) {
11416
+ abortHandler();
11417
+ return;
11418
+ }
11419
+ options.abortSignal.addEventListener("abort", abortHandler, { once: true });
11420
+ response.data.on("close", () => {
11421
+ options.abortSignal?.removeEventListener("abort", abortHandler);
11422
+ });
11423
+ }
11351
11424
  response.data.on("data", (chunk) => {
11425
+ if (options.abortSignal?.aborted) {
11426
+ return;
11427
+ }
11352
11428
  parser.feed(chunk.toString());
11353
11429
  });
11354
11430
  response.data.on("end", () => {
@@ -11360,9 +11436,23 @@ var ServerLlmBackend = class {
11360
11436
  }
11361
11437
  });
11362
11438
  response.data.on("error", (error) => {
11439
+ if (options.abortSignal?.aborted) {
11440
+ resolve3();
11441
+ return;
11442
+ }
11363
11443
  reject(error);
11364
11444
  });
11365
11445
  } catch (error) {
11446
+ if (options.abortSignal?.aborted) {
11447
+ logger.debug("[ServerLlmBackend] Request was aborted, resolving gracefully");
11448
+ resolve3();
11449
+ return;
11450
+ }
11451
+ if (isAxiosError(error) && error.code === "ERR_CANCELED") {
11452
+ logger.debug("[ServerLlmBackend] Request was canceled, resolving gracefully");
11453
+ resolve3();
11454
+ return;
11455
+ }
11366
11456
  logger.error("LLM completion failed", error);
11367
11457
  if (isAxiosError(error)) {
11368
11458
  logger.debug(
@@ -11513,8 +11603,10 @@ var ServerLlmBackend = class {
11513
11603
  logger.debug(` Body: ${logger.formatBytes(bodySize)}`);
11514
11604
  logger.debug(` Preview: ${bodyStr.substring(0, 200)}`);
11515
11605
  const response = await axiosInstance.post(this.completionsEndpoint, requestBody, {
11516
- responseType: "stream"
11606
+ responseType: "stream",
11517
11607
  // Auth header is automatically injected by ApiClient interceptor
11608
+ // Pass abort signal to cancel request if user presses ESC
11609
+ signal: options.abortSignal
11518
11610
  });
11519
11611
  logger.debug(`\u2190 ${response.status} ${response.statusText}`);
11520
11612
  return response;
@@ -11658,7 +11750,7 @@ import { isAxiosError as isAxiosError2 } from "axios";
11658
11750
  // package.json
11659
11751
  var package_default = {
11660
11752
  name: "@bike4mind/cli",
11661
- version: "0.2.11",
11753
+ version: "0.2.12-cli-todo-list.17408+244ee96b6",
11662
11754
  type: "module",
11663
11755
  description: "Interactive CLI tool for Bike4Mind with ReAct agents",
11664
11756
  license: "UNLICENSED",
@@ -11695,7 +11787,8 @@ var package_default = {
11695
11787
  test: "vitest run",
11696
11788
  "test:watch": "vitest",
11697
11789
  start: "node dist/index.js",
11698
- prepublishOnly: "pnpm build"
11790
+ prepublishOnly: "pnpm build",
11791
+ 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
11792
  },
11700
11793
  dependencies: {
11701
11794
  "@anthropic-ai/sdk": "^0.22.0",
@@ -11728,6 +11821,7 @@ var package_default = {
11728
11821
  "eventsource-parser": "^3.0.6",
11729
11822
  "file-type": "^18.7.0",
11730
11823
  "fuse.js": "^7.1.0",
11824
+ glob: "^13.0.0",
11731
11825
  "gray-matter": "^4.0.3",
11732
11826
  ink: "^6.5.1",
11733
11827
  "ink-select-input": "^6.2.0",
@@ -11761,11 +11855,11 @@ var package_default = {
11761
11855
  zustand: "^4.5.4"
11762
11856
  },
11763
11857
  devDependencies: {
11764
- "@bike4mind/agents": "workspace:*",
11765
- "@bike4mind/common": "workspace:*",
11766
- "@bike4mind/mcp": "workspace:*",
11767
- "@bike4mind/services": "workspace:*",
11768
- "@bike4mind/utils": "workspace:*",
11858
+ "@bike4mind/agents": "0.1.0",
11859
+ "@bike4mind/common": "2.41.1-cli-todo-list.17408+244ee96b6",
11860
+ "@bike4mind/mcp": "1.21.1-cli-todo-list.17408+244ee96b6",
11861
+ "@bike4mind/services": "2.36.1-cli-todo-list.17408+244ee96b6",
11862
+ "@bike4mind/utils": "2.1.6-cli-todo-list.17408+244ee96b6",
11769
11863
  "@types/better-sqlite3": "^7.6.13",
11770
11864
  "@types/diff": "^5.0.9",
11771
11865
  "@types/jsonwebtoken": "^9.0.4",
@@ -11777,7 +11871,8 @@ var package_default = {
11777
11871
  tsx: "^4.21.0",
11778
11872
  typescript: "^5.9.3",
11779
11873
  vitest: "^3.2.4"
11780
- }
11874
+ },
11875
+ gitHead: "244ee96b6f743aeff0513fc84f573050b624d4e7"
11781
11876
  };
11782
11877
 
11783
11878
  // src/config/constants.ts
@@ -12133,6 +12228,131 @@ function createSubagentDelegateTool(orchestrator, parentSessionId) {
12133
12228
  };
12134
12229
  }
12135
12230
 
12231
+ // src/tools/writeTodosTool.ts
12232
+ var VALID_STATUSES = ["pending", "in_progress", "completed", "cancelled"];
12233
+ function validateTodos(todos) {
12234
+ if (!Array.isArray(todos)) {
12235
+ throw new Error("write_todos: todos must be an array");
12236
+ }
12237
+ if (todos.length === 0) {
12238
+ throw new Error("write_todos: todos array cannot be empty");
12239
+ }
12240
+ let inProgressCount = 0;
12241
+ const validatedTodos = todos.map((item, index) => {
12242
+ if (!item || typeof item !== "object") {
12243
+ throw new Error(`write_todos: item at index ${index} must be an object`);
12244
+ }
12245
+ const { description, status } = item;
12246
+ if (typeof description !== "string" || description.trim() === "") {
12247
+ throw new Error(`write_todos: item at index ${index} must have a non-empty description`);
12248
+ }
12249
+ if (typeof status !== "string" || !VALID_STATUSES.includes(status)) {
12250
+ throw new Error(
12251
+ `write_todos: item at index ${index} has invalid status "${status}". Must be one of: ${VALID_STATUSES.join(", ")}`
12252
+ );
12253
+ }
12254
+ if (status === "in_progress") {
12255
+ inProgressCount++;
12256
+ }
12257
+ return {
12258
+ description: description.trim(),
12259
+ status
12260
+ };
12261
+ });
12262
+ if (inProgressCount > 1) {
12263
+ throw new Error(`write_todos: only one task can be 'in_progress' at a time, but found ${inProgressCount}`);
12264
+ }
12265
+ return validatedTodos;
12266
+ }
12267
+ function formatTodosOutput(todos) {
12268
+ const statusEmoji = {
12269
+ pending: "\u2B1C",
12270
+ in_progress: "\u{1F504}",
12271
+ completed: "\u2705",
12272
+ cancelled: "\u274C"
12273
+ };
12274
+ const lines = todos.map((todo, index) => {
12275
+ const emoji = statusEmoji[todo.status];
12276
+ return `${index + 1}. ${emoji} [${todo.status}] ${todo.description}`;
12277
+ });
12278
+ return lines.join("\n");
12279
+ }
12280
+ function createWriteTodosTool(todoStore) {
12281
+ return {
12282
+ toolFn: async (args) => {
12283
+ const params = args;
12284
+ const validatedTodos = validateTodos(params.todos);
12285
+ todoStore.todos = validatedTodos;
12286
+ if (todoStore.onUpdate) {
12287
+ todoStore.onUpdate(validatedTodos);
12288
+ }
12289
+ const output = formatTodosOutput(validatedTodos);
12290
+ return `Todo list updated successfully:
12291
+
12292
+ ${output}`;
12293
+ },
12294
+ toolSchema: {
12295
+ name: "write_todos",
12296
+ description: `Create or update a list of subtasks for complex multi-step requests.
12297
+
12298
+ **When to use this tool:**
12299
+ - When handling complex requests that require multiple steps
12300
+ - To break down a large task into smaller, manageable subtasks
12301
+ - To track progress through a multi-step workflow
12302
+
12303
+ **Important guidelines:**
12304
+ - Call this tool early when receiving complex requests
12305
+ - Update the list immediately when starting, completing, or cancelling tasks
12306
+ - Only ONE task should be 'in_progress' at a time
12307
+ - Never batch updates - update as soon as state changes
12308
+
12309
+ **Status values:**
12310
+ - pending: Task not yet started
12311
+ - in_progress: Currently working on this task (only ONE allowed)
12312
+ - completed: Task finished successfully
12313
+ - cancelled: Task will not be completed
12314
+
12315
+ **Example:**
12316
+ If asked to "create a new React component with tests", break it down:
12317
+ 1. Create component file \u2192 in_progress
12318
+ 2. Add component logic \u2192 pending
12319
+ 3. Create test file \u2192 pending
12320
+ 4. Write unit tests \u2192 pending`,
12321
+ parameters: {
12322
+ type: "object",
12323
+ properties: {
12324
+ todos: {
12325
+ type: "array",
12326
+ description: "Complete list of todo items. This replaces any existing list. Each item needs a description and status.",
12327
+ items: {
12328
+ type: "object",
12329
+ properties: {
12330
+ description: {
12331
+ type: "string",
12332
+ description: "Clear, concise description of the task"
12333
+ },
12334
+ status: {
12335
+ type: "string",
12336
+ enum: ["pending", "in_progress", "completed", "cancelled"],
12337
+ description: "Current status of the task. Only ONE task can be 'in_progress' at a time."
12338
+ }
12339
+ },
12340
+ required: ["description", "status"]
12341
+ }
12342
+ }
12343
+ },
12344
+ required: ["todos"]
12345
+ }
12346
+ }
12347
+ };
12348
+ }
12349
+ function createTodoStore(onUpdate) {
12350
+ return {
12351
+ todos: [],
12352
+ onUpdate
12353
+ };
12354
+ }
12355
+
12136
12356
  // src/index.tsx
12137
12357
  process.removeAllListeners("warning");
12138
12358
  process.on("warning", (warning) => {
@@ -12164,7 +12384,8 @@ function CliApp() {
12164
12384
  trustLocationSelector: null,
12165
12385
  rewindSelector: null,
12166
12386
  sessionSelector: null,
12167
- orchestrator: null
12387
+ orchestrator: null,
12388
+ abortController: null
12168
12389
  });
12169
12390
  const [isInitialized, setIsInitialized] = useState8(false);
12170
12391
  const [initError, setInitError] = useState8(null);
@@ -12210,6 +12431,15 @@ function CliApp() {
12210
12431
  }, 100);
12211
12432
  }, [state.session, state.sessionStore, state.mcpManager, state.agent, state.imageStore]);
12212
12433
  useInput6((input, key) => {
12434
+ if (key.escape) {
12435
+ if (state.abortController) {
12436
+ logger.debug("[ABORT] ESC pressed - aborting current operation...");
12437
+ state.abortController.abort();
12438
+ setState((prev) => ({ ...prev, abortController: null }));
12439
+ useCliStore.getState().setIsThinking(false);
12440
+ }
12441
+ return;
12442
+ }
12213
12443
  if (key.ctrl && input === "c") {
12214
12444
  const now = Date.now();
12215
12445
  if (exitTimestamp && now - exitTimestamp < EXIT_TIMEOUT_MS) {
@@ -12407,7 +12637,9 @@ function CliApp() {
12407
12637
  subagentConfigs
12408
12638
  });
12409
12639
  const subagentDelegateTool = createSubagentDelegateTool(orchestrator, newSession.id);
12410
- const allTools = [...b4mTools, ...mcpTools, subagentDelegateTool];
12640
+ const todoStore = createTodoStore();
12641
+ const writeTodosTool = createWriteTodosTool(todoStore);
12642
+ const allTools = [...b4mTools, ...mcpTools, subagentDelegateTool, writeTodosTool];
12411
12643
  console.log(`\u{1F916} Subagent delegation enabled (explore, plan, review)`);
12412
12644
  const projectDir = state.configStore.getProjectConfigDir();
12413
12645
  const contextResult = await loadContextFiles(projectDir);
@@ -12427,46 +12659,7 @@ function CliApp() {
12427
12659
  Follow these project-specific instructions:
12428
12660
 
12429
12661
  ${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}`;
12662
+ const cliSystemPrompt = buildCoreSystemPrompt(contextSection);
12470
12663
  const maxIterations = config.preferences.maxIterations === null ? 999999 : config.preferences.maxIterations;
12471
12664
  const agent = new ReActAgent({
12472
12665
  userId: config.userId,
@@ -12511,13 +12704,16 @@ Remember: Use context from previous messages to understand follow-up questions.$
12511
12704
  }
12512
12705
  };
12513
12706
  agent.on("thought", stepHandler);
12707
+ agent.on("observation", stepHandler);
12514
12708
  agent.on("action", stepHandler);
12515
12709
  orchestrator.setBeforeRunCallback((subagent, _subagentType) => {
12516
12710
  subagent.on("thought", stepHandler);
12711
+ subagent.on("observation", stepHandler);
12517
12712
  subagent.on("action", stepHandler);
12518
12713
  });
12519
12714
  orchestrator.setAfterRunCallback((subagent, _subagentType) => {
12520
12715
  subagent.off("thought", stepHandler);
12716
+ subagent.off("observation", stepHandler);
12521
12717
  subagent.off("action", stepHandler);
12522
12718
  });
12523
12719
  setState((prev) => ({
@@ -12564,6 +12760,8 @@ Remember: Use context from previous messages to understand follow-up questions.$
12564
12760
  return;
12565
12761
  }
12566
12762
  useCliStore.getState().setIsThinking(true);
12763
+ const abortController = new AbortController();
12764
+ setState((prev) => ({ ...prev, abortController }));
12567
12765
  const currentSteps = [];
12568
12766
  const stepHandler = (step) => {
12569
12767
  currentSteps.push(step);
@@ -12623,7 +12821,8 @@ Remember: Use context from previous messages to understand follow-up questions.$
12623
12821
  content: msg.content
12624
12822
  }));
12625
12823
  const result = await state.agent.run(messageContent, {
12626
- previousMessages: previousMessages.length > 0 ? previousMessages : void 0
12824
+ previousMessages: previousMessages.length > 0 ? previousMessages : void 0,
12825
+ signal: abortController.signal
12627
12826
  });
12628
12827
  const permissionDenied = result.finalAnswer.startsWith("Permission denied for tool");
12629
12828
  if (permissionDenied) {
@@ -12668,6 +12867,33 @@ Remember: Use context from previous messages to understand follow-up questions.$
12668
12867
  useCliStore.getState().setIsThinking(false);
12669
12868
  } catch (error) {
12670
12869
  useCliStore.getState().setIsThinking(false);
12870
+ if (error instanceof Error && error.name === "AbortError") {
12871
+ logger.debug("[ABORT] Custom command aborted by user");
12872
+ const currentSession2 = useCliStore.getState().session;
12873
+ if (currentSession2) {
12874
+ const messages2 = [...currentSession2.messages];
12875
+ const lastMessage2 = messages2[messages2.length - 1];
12876
+ if (lastMessage2 && lastMessage2.role === "assistant") {
12877
+ messages2[messages2.length - 1] = {
12878
+ ...lastMessage2,
12879
+ content: "\u26A0\uFE0F Operation cancelled by user",
12880
+ metadata: {
12881
+ ...lastMessage2.metadata,
12882
+ cancelled: true
12883
+ }
12884
+ };
12885
+ }
12886
+ const sessionWithCancel = {
12887
+ ...currentSession2,
12888
+ messages: messages2,
12889
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
12890
+ };
12891
+ setState((prev) => ({ ...prev, session: sessionWithCancel }));
12892
+ setStoreSession(sessionWithCancel);
12893
+ await state.sessionStore.save(sessionWithCancel);
12894
+ }
12895
+ return;
12896
+ }
12671
12897
  if (error?.message?.includes("Permission denied")) {
12672
12898
  console.log("\n\u26A0\uFE0F Action blocked by permission settings");
12673
12899
  const currentSession2 = useCliStore.getState().session;
@@ -12708,6 +12934,7 @@ Remember: Use context from previous messages to understand follow-up questions.$
12708
12934
  setState((prev) => ({ ...prev, session: sessionWithError }));
12709
12935
  setStoreSession(sessionWithError);
12710
12936
  } finally {
12937
+ setState((prev) => ({ ...prev, abortController: null }));
12711
12938
  state.agent.off("thought", stepHandler);
12712
12939
  state.agent.off("action", stepHandler);
12713
12940
  }
@@ -12727,6 +12954,8 @@ Remember: Use context from previous messages to understand follow-up questions.$
12727
12954
  return;
12728
12955
  }
12729
12956
  useCliStore.getState().setIsThinking(true);
12957
+ const abortController = new AbortController();
12958
+ setState((prev) => ({ ...prev, abortController }));
12730
12959
  try {
12731
12960
  let messageContent = message;
12732
12961
  let userMessageContent = message;
@@ -12764,7 +12993,8 @@ Remember: Use context from previous messages to understand follow-up questions.$
12764
12993
  content: msg.content
12765
12994
  }));
12766
12995
  const result = await state.agent.run(messageContent, {
12767
- previousMessages: previousMessages.length > 0 ? previousMessages : void 0
12996
+ previousMessages: previousMessages.length > 0 ? previousMessages : void 0,
12997
+ signal: abortController.signal
12768
12998
  });
12769
12999
  const permissionDenied = result.finalAnswer.startsWith("Permission denied for tool");
12770
13000
  if (permissionDenied) {
@@ -12804,6 +13034,30 @@ Remember: Use context from previous messages to understand follow-up questions.$
12804
13034
  await state.sessionStore.save(updatedSession);
12805
13035
  } catch (error) {
12806
13036
  useCliStore.getState().clearPendingMessages();
13037
+ if (error instanceof Error && error.name === "AbortError") {
13038
+ logger.debug("[ABORT] Operation aborted by user");
13039
+ const currentSession = useCliStore.getState().session;
13040
+ if (currentSession) {
13041
+ const cancelMessage = {
13042
+ id: uuidv410(),
13043
+ role: "assistant",
13044
+ content: "\u26A0\uFE0F Operation cancelled by user",
13045
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
13046
+ metadata: {
13047
+ cancelled: true
13048
+ }
13049
+ };
13050
+ const sessionWithCancel = {
13051
+ ...currentSession,
13052
+ messages: [...currentSession.messages, cancelMessage],
13053
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
13054
+ };
13055
+ setState((prev) => ({ ...prev, session: sessionWithCancel }));
13056
+ setStoreSession(sessionWithCancel);
13057
+ await state.sessionStore.save(sessionWithCancel);
13058
+ }
13059
+ return;
13060
+ }
12807
13061
  if (error instanceof Error) {
12808
13062
  if (error.message.includes("Authentication failed") || error.message.includes("Authentication expired")) {
12809
13063
  console.log("\n\u274C Authentication failed");
@@ -12812,6 +13066,9 @@ Remember: Use context from previous messages to understand follow-up questions.$
12812
13066
  }
12813
13067
  }
12814
13068
  console.error("Error processing message:", error);
13069
+ } finally {
13070
+ setState((prev) => ({ ...prev, abortController: null }));
13071
+ useCliStore.getState().setIsThinking(false);
12815
13072
  }
12816
13073
  };
12817
13074
  const handleBashCommand = useCallback(
@@ -12862,7 +13119,7 @@ ${output}` : output.trim() || "(no output)",
12862
13119
  if (!imageStore) {
12863
13120
  if (!imageStoreInitPromise.current) {
12864
13121
  imageStoreInitPromise.current = (async () => {
12865
- const { ImageStore: ImageStoreClass } = await import("./ImageStore-RQZ7OF7P.js");
13122
+ const { ImageStore: ImageStoreClass } = await import("./ImageStore-MMUOUPI2.js");
12866
13123
  const newImageStore = new ImageStoreClass();
12867
13124
  setState((prev) => ({
12868
13125
  ...prev,