@lobehub/chat 1.141.7 → 1.141.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/apps/desktop/package.json +1 -0
  3. package/apps/desktop/src/main/controllers/LocalFileCtr.ts +279 -52
  4. package/apps/desktop/src/main/controllers/__tests__/LocalFileCtr.test.ts +392 -0
  5. package/changelog/v1.json +18 -0
  6. package/docs/usage/features/{group-chat.mdx → agent-team.mdx} +14 -14
  7. package/docs/usage/features/agent-team.zh-CN.mdx +52 -0
  8. package/locales/ar/chat.json +17 -17
  9. package/locales/ar/setting.json +15 -19
  10. package/locales/ar/welcome.json +1 -1
  11. package/locales/bg-BG/chat.json +17 -17
  12. package/locales/bg-BG/setting.json +15 -19
  13. package/locales/de-DE/chat.json +17 -17
  14. package/locales/de-DE/setting.json +15 -19
  15. package/locales/de-DE/welcome.json +1 -1
  16. package/locales/en-US/chat.json +17 -17
  17. package/locales/en-US/setting.json +15 -19
  18. package/locales/en-US/welcome.json +1 -1
  19. package/locales/es-ES/chat.json +17 -17
  20. package/locales/es-ES/setting.json +15 -19
  21. package/locales/es-ES/welcome.json +1 -1
  22. package/locales/fa-IR/chat.json +17 -17
  23. package/locales/fa-IR/setting.json +15 -19
  24. package/locales/fa-IR/welcome.json +1 -1
  25. package/locales/fr-FR/chat.json +16 -16
  26. package/locales/fr-FR/setting.json +15 -19
  27. package/locales/fr-FR/welcome.json +1 -1
  28. package/locales/it-IT/chat.json +17 -17
  29. package/locales/it-IT/setting.json +15 -19
  30. package/locales/it-IT/welcome.json +1 -1
  31. package/locales/ja-JP/chat.json +17 -17
  32. package/locales/ja-JP/setting.json +15 -19
  33. package/locales/ja-JP/welcome.json +1 -1
  34. package/locales/ko-KR/chat.json +17 -17
  35. package/locales/ko-KR/setting.json +15 -19
  36. package/locales/ko-KR/welcome.json +1 -1
  37. package/locales/nl-NL/chat.json +17 -17
  38. package/locales/nl-NL/setting.json +15 -19
  39. package/locales/nl-NL/welcome.json +1 -1
  40. package/locales/pl-PL/chat.json +17 -17
  41. package/locales/pl-PL/setting.json +15 -19
  42. package/locales/pt-BR/chat.json +17 -17
  43. package/locales/pt-BR/setting.json +15 -19
  44. package/locales/pt-BR/welcome.json +1 -1
  45. package/locales/ru-RU/chat.json +17 -17
  46. package/locales/ru-RU/setting.json +15 -19
  47. package/locales/ru-RU/welcome.json +1 -1
  48. package/locales/tr-TR/chat.json +17 -17
  49. package/locales/tr-TR/setting.json +15 -19
  50. package/locales/vi-VN/chat.json +15 -15
  51. package/locales/vi-VN/setting.json +15 -19
  52. package/locales/zh-CN/chat.json +17 -17
  53. package/locales/zh-CN/setting.json +15 -19
  54. package/locales/zh-CN/welcome.json +1 -1
  55. package/locales/zh-TW/chat.json +17 -17
  56. package/locales/zh-TW/setting.json +15 -19
  57. package/locales/zh-TW/welcome.json +1 -1
  58. package/package.json +1 -1
  59. package/packages/agent-runtime/src/core/InterventionChecker.ts +173 -0
  60. package/packages/agent-runtime/src/core/UsageCounter.ts +248 -0
  61. package/packages/agent-runtime/src/core/__tests__/InterventionChecker.test.ts +334 -0
  62. package/packages/agent-runtime/src/core/__tests__/UsageCounter.test.ts +873 -0
  63. package/packages/agent-runtime/src/core/__tests__/runtime.test.ts +32 -26
  64. package/packages/agent-runtime/src/core/index.ts +2 -0
  65. package/packages/agent-runtime/src/core/runtime.ts +31 -18
  66. package/packages/agent-runtime/src/types/instruction.ts +1 -1
  67. package/packages/agent-runtime/src/types/state.ts +3 -3
  68. package/packages/agent-runtime/src/types/usage.ts +34 -25
  69. package/packages/const/src/settings/systemAgent.ts +0 -1
  70. package/packages/context-engine/src/index.ts +1 -0
  71. package/packages/context-engine/src/tools/ToolNameResolver.ts +2 -2
  72. package/packages/context-engine/src/tools/ToolsEngine.ts +37 -8
  73. package/packages/context-engine/src/tools/__tests__/ToolsEngine.test.ts +149 -5
  74. package/packages/context-engine/src/tools/__tests__/utils.test.ts +2 -2
  75. package/packages/context-engine/src/tools/index.ts +1 -0
  76. package/packages/context-engine/src/tools/types.ts +18 -3
  77. package/packages/context-engine/src/tools/utils.ts +4 -4
  78. package/packages/types/src/tool/builtin.ts +54 -1
  79. package/packages/types/src/tool/index.ts +1 -0
  80. package/packages/types/src/tool/intervention.ts +114 -0
  81. package/packages/types/src/user/settings/systemAgent.ts +0 -1
  82. package/packages/types/src/user/settings/tool.ts +37 -0
  83. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/ChatItem/OrchestratorThinking.tsx +2 -3
  84. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/ChatItem/index.tsx +2 -2
  85. package/src/app/[variants]/(main)/chat/(workspace)/@topic/features/GroupConfig/GroupMember.tsx +34 -2
  86. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/Main.tsx +1 -1
  87. package/src/app/[variants]/(main)/chat/(workspace)/features/{GroupChatSettings → AgentTeamSettings}/index.tsx +4 -5
  88. package/src/app/[variants]/(main)/chat/(workspace)/features/SettingButton.tsx +2 -2
  89. package/src/app/[variants]/(main)/chat/@session/_layout/Desktop/SessionHeader.tsx +2 -0
  90. package/src/app/[variants]/(main)/chat/@session/features/SessionListContent/CollapseGroup/Actions.tsx +18 -1
  91. package/src/components/ChatGroupWizard/ChatGroupWizard.tsx +33 -5
  92. package/src/components/MemberSelectionModal/MemberSelectionModal.tsx +170 -26
  93. package/src/features/Conversation/Messages/Assistant/Actions/index.tsx +7 -2
  94. package/src/features/Conversation/Messages/Assistant/Tool/Render/index.tsx +4 -2
  95. package/src/features/Conversation/Messages/User/Actions.tsx +8 -2
  96. package/src/features/GroupChatSettings/{ChatGroupSettings.tsx → AgentTeamChatSettings.tsx} +6 -5
  97. package/src/features/GroupChatSettings/{GroupMembers.tsx → AgentTeamMembersSettings.tsx} +64 -19
  98. package/src/features/GroupChatSettings/{ChatGroupMeta.tsx → AgentTeamMetaSettings.tsx} +2 -2
  99. package/src/features/GroupChatSettings/AgentTeamSettings.tsx +54 -0
  100. package/src/features/GroupChatSettings/index.ts +4 -5
  101. package/src/locales/default/chat.ts +17 -17
  102. package/src/locales/default/setting.ts +15 -19
  103. package/src/locales/default/welcome.ts +1 -1
  104. package/src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts +2 -1
  105. package/src/store/chat/slices/builtinTool/actions/{dalle.test.ts → __tests__/dalle.test.ts} +2 -5
  106. package/src/store/chat/slices/builtinTool/actions/__tests__/{localFile.test.ts → localSystem.test.ts} +4 -4
  107. package/src/store/chat/slices/builtinTool/actions/index.ts +2 -2
  108. package/src/store/chat/slices/builtinTool/actions/{localFile.ts → localSystem.ts} +183 -69
  109. package/src/store/chatGroup/action.ts +36 -1
  110. package/src/store/electron/selectors/__tests__/desktopState.test.ts +3 -3
  111. package/src/store/electron/selectors/desktopState.ts +11 -2
  112. package/src/store/user/slices/settings/selectors/__snapshots__/settings.test.ts.snap +0 -4
  113. package/src/store/user/slices/settings/selectors/systemAgent.ts +0 -2
  114. package/src/tools/local-system/Placeholder/ListFiles.tsx +10 -8
  115. package/src/tools/local-system/Placeholder/SearchFiles.tsx +12 -10
  116. package/src/tools/local-system/Placeholder/index.tsx +1 -1
  117. package/src/tools/local-system/Render/ReadLocalFile/ReadFileSkeleton.tsx +8 -18
  118. package/src/tools/local-system/Render/ReadLocalFile/ReadFileView.tsx +21 -6
  119. package/src/tools/local-system/Render/SearchFiles/Result.tsx +5 -4
  120. package/src/tools/local-system/Render/SearchFiles/SearchQuery/SearchView.tsx +4 -15
  121. package/src/tools/local-system/Render/SearchFiles/index.tsx +3 -2
  122. package/src/tools/local-system/type.ts +39 -0
  123. package/docs/usage/features/group-chat.zh-CN.mdx +0 -52
  124. package/src/features/GroupChatSettings/GroupSettings.tsx +0 -30
  125. package/src/features/GroupChatSettings/GroupSettingsContent.tsx +0 -24
  126. package/src/tools/local-system/Placeholder/ReadLocalFile.tsx +0 -9
  127. package/src/tools/local-system/Render/ReadLocalFile/style.ts +0 -37
  128. /package/src/store/chat/slices/builtinTool/actions/{search.test.ts → __tests__/search.test.ts} +0 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 1.141.9](https://github.com/lobehub/lobe-chat/compare/v1.141.8...v1.141.9)
6
+
7
+ <sup>Released on **2025-10-23**</sup>
8
+
9
+ #### 💄 Styles
10
+
11
+ - **misc**: Improve local system tools render.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### Styles
19
+
20
+ - **misc**: Improve local system tools render, closes [#9853](https://github.com/lobehub/lobe-chat/issues/9853) ([295e8fc](https://github.com/lobehub/lobe-chat/commit/295e8fc))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
30
+ ### [Version 1.141.8](https://github.com/lobehub/lobe-chat/compare/v1.141.7...v1.141.8)
31
+
32
+ <sup>Released on **2025-10-23**</sup>
33
+
34
+ #### 💄 Styles
35
+
36
+ - **misc**: Improvement for Agent Team After Alpha Launch \[LOB-517].
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### Styles
44
+
45
+ - **misc**: Improvement for Agent Team After Alpha Launch \[LOB-517], closes [#9748](https://github.com/lobehub/lobe-chat/issues/9748) ([28245be](https://github.com/lobehub/lobe-chat/commit/28245be))
46
+
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
5
55
  ### [Version 1.141.7](https://github.com/lobehub/lobe-chat/compare/v1.141.6...v1.141.7)
6
56
 
7
57
  <sup>Released on **2025-10-23**</sup>
@@ -59,6 +59,7 @@
59
59
  "electron-store": "^8.2.0",
60
60
  "electron-vite": "^3.0.0",
61
61
  "execa": "^9.5.2",
62
+ "fast-glob": "^3.3.3",
62
63
  "fix-path": "^5.0.0",
63
64
  "http-proxy-agent": "^7.0.2",
64
65
  "https-proxy-agent": "^7.0.6",
@@ -1,4 +1,10 @@
1
1
  import {
2
+ EditLocalFileParams,
3
+ EditLocalFileResult,
4
+ GlobFilesParams,
5
+ GlobFilesResult,
6
+ GrepContentParams,
7
+ GrepContentResult,
2
8
  ListLocalFileParams,
3
9
  LocalMoveFilesResultItem,
4
10
  LocalReadFileParams,
@@ -13,10 +19,10 @@ import {
13
19
  } from '@lobechat/electron-client-ipc';
14
20
  import { SYSTEM_FILES_TO_IGNORE, loadFile } from '@lobechat/file-loaders';
15
21
  import { shell } from 'electron';
16
- import * as fs from 'node:fs';
17
- import { rename as renamePromise } from 'node:fs/promises';
22
+ import fg from 'fast-glob';
23
+ import { Stats, constants } from 'node:fs';
24
+ import { access, mkdir, readFile, readdir, rename, stat, writeFile } from 'node:fs/promises';
18
25
  import * as path from 'node:path';
19
- import { promisify } from 'node:util';
20
26
 
21
27
  import FileSearchService from '@/services/fileSearchSrv';
22
28
  import { FileResult, SearchOptions } from '@/types/fileSearch';
@@ -25,40 +31,15 @@ import { createLogger } from '@/utils/logger';
25
31
 
26
32
  import { ControllerModule, ipcClientEvent } from './index';
27
33
 
28
- // 创建日志记录器
34
+ // Create logger
29
35
  const logger = createLogger('controllers:LocalFileCtr');
30
36
 
31
- const statPromise = promisify(fs.stat);
32
- const readdirPromise = promisify(fs.readdir);
33
- const renamePromiseFs = promisify(fs.rename);
34
- const accessPromise = promisify(fs.access);
35
- const writeFilePromise = promisify(fs.writeFile);
36
-
37
37
  export default class LocalFileCtr extends ControllerModule {
38
38
  private get searchService() {
39
39
  return this.app.getService(FileSearchService);
40
40
  }
41
41
 
42
- /**
43
- * Handle IPC event for local file search
44
- */
45
- @ipcClientEvent('searchLocalFiles')
46
- async handleLocalFilesSearch(params: LocalSearchFilesParams): Promise<FileResult[]> {
47
- logger.debug('Received file search request:', { keywords: params.keywords });
48
-
49
- const options: Omit<SearchOptions, 'keywords'> = {
50
- limit: 30,
51
- };
52
-
53
- try {
54
- const results = await this.searchService.search(params.keywords, options);
55
- logger.debug('File search completed', { count: results.length });
56
- return results;
57
- } catch (error) {
58
- logger.error('File search failed:', error);
59
- return [];
60
- }
61
- }
42
+ // ==================== File Operation ====================
62
43
 
63
44
  @ipcClientEvent('openLocalFile')
64
45
  async handleOpenLocalFile({ path: filePath }: OpenLocalFileParams): Promise<{
@@ -102,7 +83,7 @@ export default class LocalFileCtr extends ControllerModule {
102
83
  const results: LocalReadFileResult[] = [];
103
84
 
104
85
  for (const filePath of paths) {
105
- // 初始化结果对象
86
+ // Initialize result object
106
87
  logger.debug('Reading single file:', { filePath });
107
88
  const result = await this.readFile({ path: filePath });
108
89
  results.push(result);
@@ -158,7 +139,7 @@ export default class LocalFileCtr extends ControllerModule {
158
139
  };
159
140
 
160
141
  try {
161
- const stats = await statPromise(filePath);
142
+ const stats = await stat(filePath);
162
143
  if (stats.isDirectory()) {
163
144
  logger.warn('Attempted to read directory content:', { filePath });
164
145
  result.content = 'This is a directory and cannot be read as plain text.';
@@ -197,7 +178,7 @@ export default class LocalFileCtr extends ControllerModule {
197
178
 
198
179
  const results: FileResult[] = [];
199
180
  try {
200
- const entries = await readdirPromise(dirPath);
181
+ const entries = await readdir(dirPath);
201
182
  logger.debug('Directory entries retrieved successfully:', {
202
183
  dirPath,
203
184
  entriesCount: entries.length,
@@ -212,7 +193,7 @@ export default class LocalFileCtr extends ControllerModule {
212
193
 
213
194
  const fullPath = path.join(dirPath, entry);
214
195
  try {
215
- const stats = await statPromise(fullPath);
196
+ const stats = await stat(fullPath);
216
197
  const isDirectory = stats.isDirectory();
217
198
  results.push({
218
199
  createdTime: stats.birthtime,
@@ -260,7 +241,7 @@ export default class LocalFileCtr extends ControllerModule {
260
241
  return [];
261
242
  }
262
243
 
263
- // 逐个处理移动请求
244
+ // Process each move request
264
245
  for (const item of items) {
265
246
  const { oldPath: sourcePath, newPath } = item;
266
247
  const logPrefix = `[Moving file ${sourcePath} -> ${newPath}]`;
@@ -272,7 +253,7 @@ export default class LocalFileCtr extends ControllerModule {
272
253
  success: false,
273
254
  };
274
255
 
275
- // 基本验证
256
+ // Basic validation
276
257
  if (!sourcePath || !newPath) {
277
258
  logger.error(`${logPrefix} Parameter validation failed: source or target path is empty`);
278
259
  resultItem.error = 'Both oldPath and newPath are required for each item.';
@@ -281,9 +262,9 @@ export default class LocalFileCtr extends ControllerModule {
281
262
  }
282
263
 
283
264
  try {
284
- // 检查源是否存在
265
+ // Check if source exists
285
266
  try {
286
- await accessPromise(sourcePath, fs.constants.F_OK);
267
+ await access(sourcePath, constants.F_OK);
287
268
  logger.debug(`${logPrefix} Source file exists`);
288
269
  } catch (accessError: any) {
289
270
  if (accessError.code === 'ENOENT') {
@@ -297,28 +278,28 @@ export default class LocalFileCtr extends ControllerModule {
297
278
  }
298
279
  }
299
280
 
300
- // 检查目标路径是否与源路径相同
281
+ // Check if target path is the same as source path
301
282
  if (path.normalize(sourcePath) === path.normalize(newPath)) {
302
283
  logger.info(`${logPrefix} Source and target paths are identical, skipping move`);
303
284
  resultItem.success = true;
304
- resultItem.newPath = newPath; // 即使未移动,也报告目标路径
285
+ resultItem.newPath = newPath; // Report target path even if not moved
305
286
  results.push(resultItem);
306
287
  continue;
307
288
  }
308
289
 
309
- // LBYL: 确保目标目录存在
290
+ // LBYL: Ensure target directory exists
310
291
  const targetDir = path.dirname(newPath);
311
292
  makeSureDirExist(targetDir);
312
293
  logger.debug(`${logPrefix} Ensured target directory exists: ${targetDir}`);
313
294
 
314
- // 执行移动 (rename)
315
- await renamePromiseFs(sourcePath, newPath);
295
+ // Execute move (rename)
296
+ await rename(sourcePath, newPath);
316
297
  resultItem.success = true;
317
298
  resultItem.newPath = newPath;
318
299
  logger.info(`${logPrefix} Move successful`);
319
300
  } catch (error) {
320
301
  logger.error(`${logPrefix} Move failed:`, error);
321
- // 使用与 handleMoveFile 类似的错误处理逻辑
302
+ // Use similar error handling logic as handleMoveFile
322
303
  let errorMessage = (error as Error).message;
323
304
  if ((error as any).code === 'ENOENT')
324
305
  errorMessage = `Source path not found: ${sourcePath}.`;
@@ -334,7 +315,7 @@ export default class LocalFileCtr extends ControllerModule {
334
315
  errorMessage = `The target directory ${newPath} is not empty (relevant on some systems if target exists and is a directory).`;
335
316
  else if ((error as any).code === 'EEXIST')
336
317
  errorMessage = `An item already exists at the target path: ${newPath}.`;
337
- // 保留来自访问检查或目录检查的更具体错误
318
+ // Keep more specific errors from access or directory checks
338
319
  else if (
339
320
  !errorMessage.startsWith('Source path not found') &&
340
321
  !errorMessage.startsWith('Permission denied accessing source path') &&
@@ -411,9 +392,9 @@ export default class LocalFileCtr extends ControllerModule {
411
392
  };
412
393
  }
413
394
 
414
- // Perform the rename operation using fs.promises.rename directly
395
+ // Perform the rename operation using rename directly
415
396
  try {
416
- await renamePromise(currentPath, newPath);
397
+ await rename(currentPath, newPath);
417
398
  logger.info(`${logPrefix} Rename successful: ${currentPath} -> ${newPath}`);
418
399
  // Optionally return the newPath if frontend needs it
419
400
  // return { success: true, newPath: newPath };
@@ -444,7 +425,7 @@ export default class LocalFileCtr extends ControllerModule {
444
425
  const logPrefix = `[Writing file ${filePath}]`;
445
426
  logger.debug(`${logPrefix} Starting to write file`, { contentLength: content?.length });
446
427
 
447
- // 验证参数
428
+ // Validate parameters
448
429
  if (!filePath) {
449
430
  logger.error(`${logPrefix} Parameter validation failed: path is empty`);
450
431
  return { error: 'Path cannot be empty', success: false };
@@ -456,14 +437,14 @@ export default class LocalFileCtr extends ControllerModule {
456
437
  }
457
438
 
458
439
  try {
459
- // 确保目标目录存在
440
+ // Ensure target directory exists (use async to avoid blocking main thread)
460
441
  const dirname = path.dirname(filePath);
461
442
  logger.debug(`${logPrefix} Creating directory: ${dirname}`);
462
- fs.mkdirSync(dirname, { recursive: true });
443
+ await mkdir(dirname, { recursive: true });
463
444
 
464
- // 写入文件内容
445
+ // Write file content
465
446
  logger.debug(`${logPrefix} Starting to write content to file`);
466
- await writeFilePromise(filePath, content, 'utf8');
447
+ await writeFile(filePath, content, 'utf8');
467
448
  logger.info(`${logPrefix} File written successfully`, {
468
449
  path: filePath,
469
450
  size: content.length,
@@ -478,4 +459,250 @@ export default class LocalFileCtr extends ControllerModule {
478
459
  };
479
460
  }
480
461
  }
462
+
463
+ // ==================== Search & Find ====================
464
+
465
+ /**
466
+ * Handle IPC event for local file search
467
+ */
468
+ @ipcClientEvent('searchLocalFiles')
469
+ async handleLocalFilesSearch(params: LocalSearchFilesParams): Promise<FileResult[]> {
470
+ logger.debug('Received file search request:', { keywords: params.keywords });
471
+
472
+ const options: Omit<SearchOptions, 'keywords'> = {
473
+ limit: 30,
474
+ };
475
+
476
+ try {
477
+ const results = await this.searchService.search(params.keywords, options);
478
+ logger.debug('File search completed', { count: results.length });
479
+ return results;
480
+ } catch (error) {
481
+ logger.error('File search failed:', error);
482
+ return [];
483
+ }
484
+ }
485
+
486
+ @ipcClientEvent('grepContent')
487
+ async handleGrepContent(params: GrepContentParams): Promise<GrepContentResult> {
488
+ const {
489
+ pattern,
490
+ path: searchPath = process.cwd(),
491
+ output_mode = 'files_with_matches',
492
+ } = params;
493
+ const logPrefix = `[grepContent: ${pattern}]`;
494
+ logger.debug(`${logPrefix} Starting content search`, { output_mode, searchPath });
495
+
496
+ try {
497
+ const regex = new RegExp(
498
+ pattern,
499
+ `g${params['-i'] ? 'i' : ''}${params.multiline ? 's' : ''}`,
500
+ );
501
+
502
+ // Determine files to search
503
+ let filesToSearch: string[] = [];
504
+ const stats = await stat(searchPath);
505
+
506
+ if (stats.isFile()) {
507
+ filesToSearch = [searchPath];
508
+ } else {
509
+ // Use glob pattern if provided, otherwise search all files
510
+ const globPattern = params.glob || '**/*';
511
+ filesToSearch = await fg(globPattern, {
512
+ absolute: true,
513
+ cwd: searchPath,
514
+ dot: true,
515
+ ignore: ['**/node_modules/**', '**/.git/**'],
516
+ });
517
+
518
+ // Filter by type if provided
519
+ if (params.type) {
520
+ const ext = `.${params.type}`;
521
+ filesToSearch = filesToSearch.filter((file) => file.endsWith(ext));
522
+ }
523
+ }
524
+
525
+ logger.debug(`${logPrefix} Found ${filesToSearch.length} files to search`);
526
+
527
+ const matches: string[] = [];
528
+ let totalMatches = 0;
529
+
530
+ for (const filePath of filesToSearch) {
531
+ try {
532
+ const fileStats = await stat(filePath);
533
+ if (!fileStats.isFile()) continue;
534
+
535
+ const content = await readFile(filePath, 'utf8');
536
+ const lines = content.split('\n');
537
+
538
+ switch (output_mode) {
539
+ case 'files_with_matches': {
540
+ if (regex.test(content)) {
541
+ matches.push(filePath);
542
+ totalMatches++;
543
+ if (params.head_limit && matches.length >= params.head_limit) break;
544
+ }
545
+ break;
546
+ }
547
+ case 'content': {
548
+ const matchedLines: string[] = [];
549
+ for (let i = 0; i < lines.length; i++) {
550
+ if (regex.test(lines[i])) {
551
+ const contextBefore = params['-B'] || params['-C'] || 0;
552
+ const contextAfter = params['-A'] || params['-C'] || 0;
553
+
554
+ const startLine = Math.max(0, i - contextBefore);
555
+ const endLine = Math.min(lines.length - 1, i + contextAfter);
556
+
557
+ for (let j = startLine; j <= endLine; j++) {
558
+ const lineNum = params['-n'] ? `${j + 1}:` : '';
559
+ matchedLines.push(`${filePath}:${lineNum}${lines[j]}`);
560
+ }
561
+ totalMatches++;
562
+ }
563
+ }
564
+ matches.push(...matchedLines);
565
+ if (params.head_limit && matches.length >= params.head_limit) break;
566
+ break;
567
+ }
568
+ case 'count': {
569
+ const fileMatches = (content.match(regex) || []).length;
570
+ if (fileMatches > 0) {
571
+ matches.push(`${filePath}:${fileMatches}`);
572
+ totalMatches += fileMatches;
573
+ }
574
+ break;
575
+ }
576
+ }
577
+ } catch (error) {
578
+ logger.debug(`${logPrefix} Skipping file ${filePath}:`, error);
579
+ }
580
+ }
581
+
582
+ logger.info(`${logPrefix} Search completed`, {
583
+ matchCount: matches.length,
584
+ totalMatches,
585
+ });
586
+
587
+ return {
588
+ matches: params.head_limit ? matches.slice(0, params.head_limit) : matches,
589
+ success: true,
590
+ total_matches: totalMatches,
591
+ };
592
+ } catch (error) {
593
+ logger.error(`${logPrefix} Grep failed:`, error);
594
+ return {
595
+ matches: [],
596
+ success: false,
597
+ total_matches: 0,
598
+ };
599
+ }
600
+ }
601
+
602
+ @ipcClientEvent('globLocalFiles')
603
+ async handleGlobFiles({
604
+ path: searchPath = process.cwd(),
605
+ pattern,
606
+ }: GlobFilesParams): Promise<GlobFilesResult> {
607
+ const logPrefix = `[globFiles: ${pattern}]`;
608
+ logger.debug(`${logPrefix} Starting glob search`, { searchPath });
609
+
610
+ try {
611
+ const files = await fg(pattern, {
612
+ absolute: true,
613
+ cwd: searchPath,
614
+ dot: true,
615
+ onlyFiles: false,
616
+ stats: true,
617
+ });
618
+
619
+ // Sort by modification time (most recent first)
620
+ const sortedFiles = (files as unknown as Array<{ path: string; stats: Stats }>)
621
+ .sort((a, b) => b.stats.mtime.getTime() - a.stats.mtime.getTime())
622
+ .map((f) => f.path);
623
+
624
+ logger.info(`${logPrefix} Glob completed`, { fileCount: sortedFiles.length });
625
+
626
+ return {
627
+ files: sortedFiles,
628
+ success: true,
629
+ total_files: sortedFiles.length,
630
+ };
631
+ } catch (error) {
632
+ logger.error(`${logPrefix} Glob failed:`, error);
633
+ return {
634
+ files: [],
635
+ success: false,
636
+ total_files: 0,
637
+ };
638
+ }
639
+ }
640
+
641
+ // ==================== File Editing ====================
642
+
643
+ @ipcClientEvent('editLocalFile')
644
+ async handleEditFile({
645
+ file_path: filePath,
646
+ new_string,
647
+ old_string,
648
+ replace_all = false,
649
+ }: EditLocalFileParams): Promise<EditLocalFileResult> {
650
+ const logPrefix = `[editFile: ${filePath}]`;
651
+ logger.debug(`${logPrefix} Starting file edit`, { replace_all });
652
+
653
+ try {
654
+ // Read file content
655
+ const content = await readFile(filePath, 'utf8');
656
+
657
+ // Check if old_string exists
658
+ if (!content.includes(old_string)) {
659
+ logger.error(`${logPrefix} Old string not found in file`);
660
+ return {
661
+ error: 'The specified old_string was not found in the file',
662
+ replacements: 0,
663
+ success: false,
664
+ };
665
+ }
666
+
667
+ // Perform replacement
668
+ let newContent: string;
669
+ let replacements: number;
670
+
671
+ if (replace_all) {
672
+ const regex = new RegExp(old_string.replaceAll(/[$()*+.?[\\\]^{|}]/g, '\\$&'), 'g');
673
+ const matches = content.match(regex);
674
+ replacements = matches ? matches.length : 0;
675
+ newContent = content.replaceAll(old_string, new_string);
676
+ } else {
677
+ // Replace only first occurrence
678
+ const index = content.indexOf(old_string);
679
+ if (index === -1) {
680
+ return {
681
+ error: 'Old string not found',
682
+ replacements: 0,
683
+ success: false,
684
+ };
685
+ }
686
+ newContent =
687
+ content.slice(0, index) + new_string + content.slice(index + old_string.length);
688
+ replacements = 1;
689
+ }
690
+
691
+ // Write back to file
692
+ await writeFile(filePath, newContent, 'utf8');
693
+
694
+ logger.info(`${logPrefix} File edited successfully`, { replacements });
695
+ return {
696
+ replacements,
697
+ success: true,
698
+ };
699
+ } catch (error) {
700
+ logger.error(`${logPrefix} Edit failed:`, error);
701
+ return {
702
+ error: (error as Error).message,
703
+ replacements: 0,
704
+ success: false,
705
+ };
706
+ }
707
+ }
481
708
  }