openhorse 0.2.8 → 0.2.10

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 (184) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +10 -1
  3. package/dist/cli.d.ts +1 -1
  4. package/dist/cli.js +9 -16
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/index.d.ts.map +1 -1
  7. package/dist/commands/index.js +70 -26
  8. package/dist/commands/index.js.map +1 -1
  9. package/dist/commands/types.d.ts +7 -28
  10. package/dist/commands/types.d.ts.map +1 -1
  11. package/dist/commands/types.js.map +1 -1
  12. package/dist/framework/prompt.d.ts.map +1 -1
  13. package/dist/framework/prompt.js +15 -2
  14. package/dist/framework/prompt.js.map +1 -1
  15. package/dist/framework/query.d.ts +2 -0
  16. package/dist/framework/query.d.ts.map +1 -1
  17. package/dist/framework/query.js +82 -4
  18. package/dist/framework/query.js.map +1 -1
  19. package/dist/framework/tool-scheduler.d.ts +3 -0
  20. package/dist/framework/tool-scheduler.d.ts.map +1 -1
  21. package/dist/framework/tool-scheduler.js +9 -0
  22. package/dist/framework/tool-scheduler.js.map +1 -1
  23. package/dist/ink-ui/App.d.ts +2 -2
  24. package/dist/ink-ui/App.d.ts.map +1 -1
  25. package/dist/ink-ui/components/PixelHorseBanner.d.ts +2 -2
  26. package/dist/ink-ui/components/PixelHorseBanner.d.ts.map +1 -1
  27. package/dist/ink-ui/components/RunningHorseIndicator.d.ts.map +1 -1
  28. package/dist/ink-ui/components/RunningHorseIndicator.js +2 -1
  29. package/dist/ink-ui/components/RunningHorseIndicator.js.map +1 -1
  30. package/dist/ink-ui/components/StatusLine.d.ts +2 -2
  31. package/dist/ink-ui/components/StatusLine.d.ts.map +1 -1
  32. package/dist/ink-ui/launch.d.ts +2 -2
  33. package/dist/ink-ui/launch.d.ts.map +1 -1
  34. package/dist/ink-ui/launch.js.map +1 -1
  35. package/dist/ink-ui/screens/ReplScreen.d.ts +2 -2
  36. package/dist/ink-ui/screens/ReplScreen.d.ts.map +1 -1
  37. package/dist/ink-ui/screens/ReplScreen.js +4 -2
  38. package/dist/ink-ui/screens/ReplScreen.js.map +1 -1
  39. package/dist/print-ui/launch.d.ts +44 -2
  40. package/dist/print-ui/launch.d.ts.map +1 -1
  41. package/dist/print-ui/launch.js +12 -0
  42. package/dist/print-ui/launch.js.map +1 -1
  43. package/dist/runtime/agent-runtime-controller.d.ts +4 -1
  44. package/dist/runtime/agent-runtime-controller.d.ts.map +1 -1
  45. package/dist/runtime/agent-runtime-controller.js +34 -2
  46. package/dist/runtime/agent-runtime-controller.js.map +1 -1
  47. package/dist/runtime/agent-runtime-protocol.d.ts +7 -1
  48. package/dist/runtime/agent-runtime-protocol.d.ts.map +1 -1
  49. package/dist/runtime/agent-runtime-protocol.js +12 -0
  50. package/dist/runtime/agent-runtime-protocol.js.map +1 -1
  51. package/dist/runtime/agent-status.d.ts +4 -0
  52. package/dist/runtime/agent-status.d.ts.map +1 -0
  53. package/dist/runtime/agent-status.js +15 -0
  54. package/dist/runtime/agent-status.js.map +1 -0
  55. package/dist/runtime/chat-controller.d.ts +4 -1
  56. package/dist/runtime/chat-controller.d.ts.map +1 -1
  57. package/dist/runtime/chat-controller.js +35 -3
  58. package/dist/runtime/chat-controller.js.map +1 -1
  59. package/dist/runtime/ui-events.d.ts +29 -2
  60. package/dist/runtime/ui-events.d.ts.map +1 -1
  61. package/dist/runtime/ui-events.js +34 -0
  62. package/dist/runtime/ui-events.js.map +1 -1
  63. package/dist/services/compact/auto-compact.d.ts +19 -0
  64. package/dist/services/compact/auto-compact.d.ts.map +1 -1
  65. package/dist/services/compact/auto-compact.js +55 -7
  66. package/dist/services/compact/auto-compact.js.map +1 -1
  67. package/dist/services/config-dir.d.ts +2 -0
  68. package/dist/services/config-dir.d.ts.map +1 -1
  69. package/dist/services/config-dir.js +5 -0
  70. package/dist/services/config-dir.js.map +1 -1
  71. package/dist/services/config.d.ts +9 -2
  72. package/dist/services/config.d.ts.map +1 -1
  73. package/dist/services/config.js +45 -11
  74. package/dist/services/config.js.map +1 -1
  75. package/dist/services/doctor.d.ts.map +1 -1
  76. package/dist/services/doctor.js +36 -4
  77. package/dist/services/doctor.js.map +1 -1
  78. package/dist/services/format.d.ts +2 -0
  79. package/dist/services/format.d.ts.map +1 -0
  80. package/dist/services/format.js +20 -0
  81. package/dist/services/format.js.map +1 -0
  82. package/dist/services/global-config.d.ts +10 -9
  83. package/dist/services/global-config.d.ts.map +1 -1
  84. package/dist/services/global-config.js +5 -20
  85. package/dist/services/global-config.js.map +1 -1
  86. package/dist/services/llm.d.ts +2 -0
  87. package/dist/services/llm.d.ts.map +1 -1
  88. package/dist/services/llm.js +57 -20
  89. package/dist/services/llm.js.map +1 -1
  90. package/dist/services/model-context.d.ts +10 -1
  91. package/dist/services/model-context.d.ts.map +1 -1
  92. package/dist/services/model-context.js +53 -14
  93. package/dist/services/model-context.js.map +1 -1
  94. package/dist/services/storage-maintenance.d.ts.map +1 -1
  95. package/dist/services/storage-maintenance.js +3 -9
  96. package/dist/services/storage-maintenance.js.map +1 -1
  97. package/dist/services/usage-state.d.ts +20 -0
  98. package/dist/services/usage-state.d.ts.map +1 -0
  99. package/dist/services/usage-state.js +128 -0
  100. package/dist/services/usage-state.js.map +1 -0
  101. package/dist/terminal-ui/launch.d.ts +44 -2
  102. package/dist/terminal-ui/launch.d.ts.map +1 -1
  103. package/dist/terminal-ui/launch.js +102 -36
  104. package/dist/terminal-ui/launch.js.map +1 -1
  105. package/dist/terminal-ui/raw-editor.d.ts +2 -0
  106. package/dist/terminal-ui/raw-editor.d.ts.map +1 -1
  107. package/dist/terminal-ui/raw-editor.js +30 -1
  108. package/dist/terminal-ui/raw-editor.js.map +1 -1
  109. package/dist/tools/index.d.ts.map +1 -1
  110. package/dist/tools/index.js +259 -43
  111. package/dist/tools/index.js.map +1 -1
  112. package/dist/tui-core/input-parser.d.ts +4 -0
  113. package/dist/tui-core/input-parser.d.ts.map +1 -1
  114. package/dist/tui-core/input-parser.js +30 -0
  115. package/dist/tui-core/input-parser.js.map +1 -1
  116. package/dist/tui-ui/launch.d.ts +2 -2
  117. package/dist/tui-ui/launch.d.ts.map +1 -1
  118. package/dist/tui-ui/launch.js +2 -0
  119. package/dist/tui-ui/launch.js.map +1 -1
  120. package/dist/tui-ui/layout.d.ts.map +1 -1
  121. package/dist/tui-ui/layout.js +2 -13
  122. package/dist/tui-ui/layout.js.map +1 -1
  123. package/dist/tui-ui/runner.d.ts.map +1 -1
  124. package/dist/tui-ui/runner.js +6 -0
  125. package/dist/tui-ui/runner.js.map +1 -1
  126. package/dist/tui-ui/state.d.ts +13 -1
  127. package/dist/tui-ui/state.d.ts.map +1 -1
  128. package/dist/tui-ui/state.js +13 -0
  129. package/dist/tui-ui/state.js.map +1 -1
  130. package/dist/ui/command-panel.d.ts +1 -1
  131. package/dist/ui/command-panel.d.ts.map +1 -1
  132. package/dist/ui/command-panel.js +37 -34
  133. package/dist/ui/command-panel.js.map +1 -1
  134. package/dist/ui/file-completion.d.ts +1 -1
  135. package/dist/ui/file-completion.d.ts.map +1 -1
  136. package/dist/ui/file-completion.js +4 -4
  137. package/dist/ui/file-completion.js.map +1 -1
  138. package/dist/ui/shared/command-palette.d.ts +3 -0
  139. package/dist/ui/shared/command-palette.d.ts.map +1 -0
  140. package/dist/ui/shared/command-palette.js +54 -0
  141. package/dist/ui/shared/command-palette.js.map +1 -0
  142. package/dist/ui/shared/command-suggestions.d.ts +19 -0
  143. package/dist/ui/shared/command-suggestions.d.ts.map +1 -0
  144. package/dist/ui/shared/command-suggestions.js +67 -0
  145. package/dist/ui/shared/command-suggestions.js.map +1 -0
  146. package/dist/ui/shared/input-frame.d.ts +19 -0
  147. package/dist/ui/shared/input-frame.d.ts.map +1 -0
  148. package/dist/ui/shared/input-frame.js +74 -0
  149. package/dist/ui/shared/input-frame.js.map +1 -0
  150. package/dist/ui/shared/text.d.ts +5 -0
  151. package/dist/ui/shared/text.d.ts.map +1 -0
  152. package/dist/ui/shared/text.js +55 -0
  153. package/dist/ui/shared/text.js.map +1 -0
  154. package/dist/ui/shared/types.d.ts +27 -0
  155. package/dist/ui/shared/types.d.ts.map +1 -0
  156. package/dist/ui/shared/types.js +3 -0
  157. package/dist/ui/shared/types.js.map +1 -0
  158. package/dist/ui/user-input.js +2 -2
  159. package/dist/ui/user-input.js.map +1 -1
  160. package/dist/ui-v2/components/command-palette.d.ts +1 -5
  161. package/dist/ui-v2/components/command-palette.d.ts.map +1 -1
  162. package/dist/ui-v2/components/command-palette.js +15 -54
  163. package/dist/ui-v2/components/command-palette.js.map +1 -1
  164. package/dist/ui-v2/components/shell.d.ts +1 -14
  165. package/dist/ui-v2/components/shell.d.ts.map +1 -1
  166. package/dist/ui-v2/components/shell.js +4 -55
  167. package/dist/ui-v2/components/shell.js.map +1 -1
  168. package/dist/ui-v2/runtime/text.d.ts +1 -7
  169. package/dist/ui-v2/runtime/text.d.ts.map +1 -1
  170. package/dist/ui-v2/runtime/text.js +15 -55
  171. package/dist/ui-v2/runtime/text.js.map +1 -1
  172. package/dist/ui-v2/state/sessions.d.ts +1 -1
  173. package/dist/ui-v2/state/sessions.d.ts.map +1 -1
  174. package/dist/ui-v2/state/sessions.js +5 -18
  175. package/dist/ui-v2/state/sessions.js.map +1 -1
  176. package/dist/ui-v2/state/suggestions.d.ts +1 -21
  177. package/dist/ui-v2/state/suggestions.d.ts.map +1 -1
  178. package/dist/ui-v2/state/suggestions.js +15 -67
  179. package/dist/ui-v2/state/suggestions.js.map +1 -1
  180. package/dist/ui-v2/types.d.ts +1 -32
  181. package/dist/ui-v2/types.d.ts.map +1 -1
  182. package/dist/ui-v2/types.js +15 -6
  183. package/dist/ui-v2/types.js.map +1 -1
  184. package/package.json +4 -1
@@ -32,6 +32,9 @@ const plan_1 = require("./plan");
32
32
  const git_1 = require("./git");
33
33
  const lsp_1 = require("./lsp");
34
34
  const bash_security_1 = require("./bash_security");
35
+ const BATCH_READ_ALLOWED_TOOLS = new Set(['git_status', 'list_files', 'glob', 'grep', 'read_file']);
36
+ const BATCH_READ_MAX_STEPS = 8;
37
+ const BATCH_READ_STEP_OUTPUT_MAX_BYTES = 1600;
35
38
  // ============================================================================
36
39
  // 工具集
37
40
  // ============================================================================
@@ -72,9 +75,10 @@ exports.TOOLS = [
72
75
  if (!path || typeof path !== 'string') {
73
76
  return { success: false, output: '', error: 'read_file requires a path parameter' };
74
77
  }
75
- return readFileSync_(path, args.maxLines);
78
+ return readFileSync_(path, args.maxLines, context.cwd);
76
79
  },
77
80
  isReadOnly: () => true,
81
+ isConcurrencySafe: () => true,
78
82
  userFacingName: (args) => `Read ${args.path}`,
79
83
  getSummary: (args, result) => {
80
84
  const path = args.path;
@@ -112,7 +116,7 @@ exports.TOOLS = [
112
116
  if (typeof content !== 'string') {
113
117
  return { success: false, output: '', error: 'write_file requires a content parameter' };
114
118
  }
115
- return writeFileSync_(path, content);
119
+ return writeFileSync_(path, content, context.cwd);
116
120
  },
117
121
  isDestructive: () => true,
118
122
  checkPermissions: (args, context) => {
@@ -145,13 +149,13 @@ exports.TOOLS = [
145
149
  },
146
150
  required: ['path'],
147
151
  },
148
- execute: async (args) => {
152
+ execute: async (args, context) => {
149
153
  // Ensure path is a valid string
150
154
  const path = args.path;
151
155
  if (!path || typeof path !== 'string') {
152
156
  return { success: false, output: '', error: 'list_files requires a path parameter' };
153
157
  }
154
- return listFiles_(path, args.maxDepth);
158
+ return listFiles_(path, args.maxDepth, context.cwd);
155
159
  },
156
160
  isReadOnly: () => true,
157
161
  isConcurrencySafe: () => true,
@@ -196,7 +200,7 @@ exports.TOOLS = [
196
200
  return { success: false, output: '', error: 'exec_command requires a command parameter' };
197
201
  }
198
202
  // Issue #32 #3.2: 传递 abortSignal
199
- return execCommand_(command, args.cwd, args.timeout, args.maxOutput, context.abortSignal);
203
+ return execCommand_(command, args.cwd, args.timeout, args.maxOutput, context.abortSignal, context.cwd);
200
204
  },
201
205
  isDestructive: (args) => {
202
206
  const cmd = args.command || '';
@@ -222,6 +226,10 @@ exports.TOOLS = [
222
226
  const cmd = args.command || '';
223
227
  return (0, bash_security_1.isReadOnlyCommand)(cmd);
224
228
  },
229
+ isConcurrencySafe: (args) => {
230
+ const cmd = args.command || '';
231
+ return (0, bash_security_1.isReadOnlyCommand)(cmd);
232
+ },
225
233
  userFacingName: (args) => `Exec ${args.command?.slice(0, 60) || ''}`,
226
234
  getSummary: (args, result) => {
227
235
  const cmd = args.command?.slice(0, 40) || '';
@@ -289,7 +297,7 @@ exports.TOOLS = [
289
297
  lastEditFileArgs.turnId = context.turnId;
290
298
  lastEditFileArgs.updatedAt = Date.now();
291
299
  (0, tool_state_1.setToolState)({ lastEditFileArgs });
292
- return editFile_(path, old_string, new_string, args.replace_all, args.fuzzy_match, args.preview);
300
+ return editFile_(path, old_string, new_string, args.replace_all, args.fuzzy_match, args.preview, context.cwd);
293
301
  },
294
302
  isDestructive: () => true,
295
303
  checkPermissions: (args, context) => {
@@ -321,13 +329,13 @@ exports.TOOLS = [
321
329
  },
322
330
  required: ['pattern'],
323
331
  },
324
- execute: async (args) => {
332
+ execute: async (args, context) => {
325
333
  // Ensure pattern is a valid string
326
334
  const pattern = args.pattern;
327
335
  if (!pattern || typeof pattern !== 'string') {
328
336
  return { success: false, output: '', error: 'glob requires a pattern parameter' };
329
337
  }
330
- return glob_(pattern, args.path);
338
+ return glob_(pattern, args.path, context.cwd);
331
339
  },
332
340
  isReadOnly: () => true,
333
341
  isConcurrencySafe: () => true,
@@ -365,13 +373,13 @@ exports.TOOLS = [
365
373
  },
366
374
  required: ['pattern'],
367
375
  },
368
- execute: async (args) => {
376
+ execute: async (args, context) => {
369
377
  // Ensure pattern is a valid string
370
378
  const pattern = args.pattern;
371
379
  if (!pattern || typeof pattern !== 'string') {
372
380
  return { success: false, output: '', error: 'grep requires a pattern parameter' };
373
381
  }
374
- return grep_(pattern, args.path, args.glob, args.context);
382
+ return grep_(pattern, args.path, args.glob, args.context, context.cwd);
375
383
  },
376
384
  isReadOnly: () => true,
377
385
  isConcurrencySafe: () => true,
@@ -384,6 +392,46 @@ exports.TOOLS = [
384
392
  return `🔎 grep /${pattern}/ → ${count} matches`;
385
393
  },
386
394
  }),
395
+ (0, tool_1.buildTool)({
396
+ name: 'batch_read',
397
+ description: 'Run up to 8 read-only exploration tool calls in one ordered batch. Allowed tools: git_status, list_files, glob, grep, read_file.',
398
+ parameters: {
399
+ type: 'object',
400
+ properties: {
401
+ steps: {
402
+ type: 'array',
403
+ description: 'Array of steps: [{ "tool": "read_file", "args": { "path": "package.json" } }]. Max 8 steps.',
404
+ items: {
405
+ type: 'object',
406
+ properties: {
407
+ tool: { type: 'string', description: 'Allowed read-only tool name' },
408
+ args: { type: 'object', description: 'Arguments for the tool' },
409
+ },
410
+ required: ['tool', 'args'],
411
+ },
412
+ maxItems: BATCH_READ_MAX_STEPS,
413
+ },
414
+ reason: {
415
+ type: 'string',
416
+ description: 'Optional reason for this read-only batch.',
417
+ },
418
+ },
419
+ required: ['steps'],
420
+ },
421
+ execute: async (args, context) => executeBatchRead(args, context),
422
+ isReadOnly: () => true,
423
+ isConcurrencySafe: () => true,
424
+ userFacingName: (args) => {
425
+ const count = Array.isArray(args.steps) ? args.steps.length : 0;
426
+ return `Batch read ${count} steps`;
427
+ },
428
+ getSummary: (args, result) => {
429
+ if (!result.success)
430
+ return '📚 batch_read → error';
431
+ const count = Array.isArray(args.steps) ? args.steps.length : 0;
432
+ return `📚 batch_read → ${count} steps`;
433
+ },
434
+ }),
387
435
  // Memory tools
388
436
  (0, tool_1.buildTool)({
389
437
  name: 'memory_save',
@@ -710,23 +758,55 @@ function getRuntimeTools() {
710
758
  // ============================================================================
711
759
  // 工具实现
712
760
  // ============================================================================
713
- /** 安全路径解析 防止路径遍历攻击 */
714
- function safePath(input) {
715
- const resolved = (0, path_1.resolve)(input);
716
- const cwd = process.cwd();
717
- if ((0, path_1.relative)(cwd, resolved).startsWith('..')) {
718
- return cwd;
761
+ /** Normalize model/tool path strings before resolving them on disk. */
762
+ function normalizeToolPath(input) {
763
+ let value = input.trim();
764
+ const markdownLink = value.match(/^!?\[[^\]]*\]\(([\s\S]+)\)$/u);
765
+ if (markdownLink) {
766
+ value = markdownLink[1].trim();
767
+ if (value.startsWith('<')) {
768
+ const end = value.indexOf('>');
769
+ if (end >= 0) {
770
+ value = value.slice(1, end);
771
+ }
772
+ }
773
+ else {
774
+ value = value.replace(/\s+["'][\s\S]*["']$/u, '');
775
+ }
776
+ }
777
+ if ((value.startsWith('`') && value.endsWith('`')) ||
778
+ (value.startsWith('"') && value.endsWith('"')) ||
779
+ (value.startsWith("'") && value.endsWith("'"))) {
780
+ value = value.slice(1, -1);
781
+ }
782
+ if (value.startsWith('file://')) {
783
+ try {
784
+ return decodeURIComponent(new URL(value).pathname);
785
+ }
786
+ catch {
787
+ value = value.replace(/^file:\/\//u, '');
788
+ }
789
+ }
790
+ try {
791
+ return decodeURIComponent(value);
792
+ }
793
+ catch {
794
+ return value;
719
795
  }
720
- return resolved;
721
796
  }
722
- async function readFileSync_(path, maxLines) {
797
+ /** Resolve tool path parameters relative to the current tool cwd. */
798
+ function safePath(input, cwd = process.cwd()) {
799
+ return (0, path_1.resolve)(cwd, normalizeToolPath(input));
800
+ }
801
+ async function readFileSync_(path, maxLines, cwd) {
723
802
  try {
724
- const resolved = safePath(path);
803
+ const normalizedPath = normalizeToolPath(path);
804
+ const resolved = safePath(path, cwd);
725
805
  if (!(0, fs_1.existsSync)(resolved)) {
726
- return { success: false, output: '', error: `File not found: ${path}` };
806
+ return { success: false, output: '', error: `File not found: ${normalizedPath}` };
727
807
  }
728
808
  if ((0, fs_1.statSync)(resolved).isDirectory()) {
729
- return { success: false, output: '', error: `Path is a directory, not a file: ${path}` };
809
+ return { success: false, output: '', error: `Path is a directory, not a file: ${normalizedPath}` };
730
810
  }
731
811
  const content = (0, fs_1.readFileSync)(resolved, 'utf-8');
732
812
  const lines = content.split('\n');
@@ -755,23 +835,25 @@ async function readFileSync_(path, maxLines) {
755
835
  return { success: false, output: '', error: String(err.message) };
756
836
  }
757
837
  }
758
- async function writeFileSync_(path, content) {
838
+ async function writeFileSync_(path, content, cwd) {
759
839
  try {
760
- const resolved = safePath(path);
840
+ const normalizedPath = normalizeToolPath(path);
841
+ const resolved = safePath(path, cwd);
761
842
  (0, fs_1.writeFileSync)(resolved, content, 'utf-8');
762
- return { success: true, output: `Wrote ${content.split('\n').length} lines to ${path}` };
843
+ return { success: true, output: `Wrote ${content.split('\n').length} lines to ${normalizedPath}` };
763
844
  }
764
845
  catch (err) {
765
846
  return { success: false, output: '', error: String(err.message) };
766
847
  }
767
848
  }
768
- async function listFiles_(path, maxDepth) {
769
- const resolved = safePath(path);
849
+ async function listFiles_(path, maxDepth, cwd) {
850
+ const normalizedPath = normalizeToolPath(path);
851
+ const resolved = safePath(path, cwd);
770
852
  if (!(0, fs_1.existsSync)(resolved)) {
771
- return { success: false, output: '', error: `Path not found: ${path}` };
853
+ return { success: false, output: '', error: `Path not found: ${normalizedPath}` };
772
854
  }
773
855
  if (!(0, fs_1.statSync)(resolved).isDirectory()) {
774
- return { success: true, output: path };
856
+ return { success: true, output: normalizedPath };
775
857
  }
776
858
  const depth = maxDepth ?? 2;
777
859
  const results = [];
@@ -807,9 +889,9 @@ async function listFiles_(path, maxDepth) {
807
889
  return { success: true, output };
808
890
  }
809
891
  // Issue #32 #3.2: execCommand_ 支持 abortSignal
810
- async function execCommand_(command, cwd, timeout, maxOutput, abortSignal) {
892
+ async function execCommand_(command, cwd, timeout, maxOutput, abortSignal, baseCwd) {
811
893
  return new Promise((resolve) => {
812
- const workdir = cwd ? safePath(cwd) : process.cwd();
894
+ const workdir = cwd ? safePath(cwd, baseCwd) : baseCwd ?? process.cwd();
813
895
  const timeoutMs = timeout ?? 30000;
814
896
  const maxBytes = maxOutput ?? 51200; // Default 50KB, Issue #28 fix
815
897
  // Use spawn for streaming output with truncation support
@@ -1072,14 +1154,15 @@ function fuzzyMatch(content, oldString) {
1072
1154
  }
1073
1155
  return null;
1074
1156
  }
1075
- async function editFile_(path, old_string, new_string, replace_all, fuzzy_match, preview) {
1157
+ async function editFile_(path, old_string, new_string, replace_all, fuzzy_match, preview, cwd) {
1076
1158
  try {
1077
- const resolved = safePath(path);
1159
+ const normalizedPath = normalizeToolPath(path);
1160
+ const resolved = safePath(path, cwd);
1078
1161
  if (!(0, fs_1.existsSync)(resolved)) {
1079
- return { success: false, output: '', error: `File not found: ${path}` };
1162
+ return { success: false, output: '', error: `File not found: ${normalizedPath}` };
1080
1163
  }
1081
1164
  if ((0, fs_1.statSync)(resolved).isDirectory()) {
1082
- return { success: false, output: '', error: `Path is a directory, not a file: ${path}` };
1165
+ return { success: false, output: '', error: `Path is a directory, not a file: ${normalizedPath}` };
1083
1166
  }
1084
1167
  const content = (0, fs_1.readFileSync)(resolved, 'utf-8');
1085
1168
  // Check if old_string exists exactly
@@ -1154,7 +1237,7 @@ async function editFile_(path, old_string, new_string, replace_all, fuzzy_match,
1154
1237
  (0, fs_1.writeFileSync)(resolved, newContent, 'utf-8');
1155
1238
  return {
1156
1239
  success: true,
1157
- output: `Fuzzy edited ${path} (matched by ${fuzzyResult.strategy}, "${match.slice(0, 50)}...")`,
1240
+ output: `Fuzzy edited ${normalizedPath} (matched by ${fuzzyResult.strategy}, "${match.slice(0, 50)}...")`,
1158
1241
  };
1159
1242
  }
1160
1243
  // If not replace_all, require unique match
@@ -1178,7 +1261,7 @@ async function editFile_(path, old_string, new_string, replace_all, fuzzy_match,
1178
1261
  (0, fs_1.writeFileSync)(resolved, newContent, 'utf-8');
1179
1262
  return {
1180
1263
  success: true,
1181
- output: `Replaced ${count} occurrence(s) of old_string with new_string in ${path}`,
1264
+ output: `Replaced ${count} occurrence(s) of old_string with new_string in ${normalizedPath}`,
1182
1265
  };
1183
1266
  }
1184
1267
  catch (err) {
@@ -1196,14 +1279,15 @@ function escapeRegExp(str) {
1196
1279
  * Glob 模式匹配 - 简化版实现
1197
1280
  * 支持: **(递归目录)、*(任意字符)、?(单个字符)
1198
1281
  */
1199
- async function glob_(pattern, basePath) {
1282
+ async function glob_(pattern, basePath, cwd) {
1200
1283
  try {
1201
- const base = basePath ? safePath(basePath) : process.cwd();
1284
+ const normalizedBasePath = basePath ? normalizeToolPath(basePath) : cwd ?? process.cwd();
1285
+ const base = basePath ? safePath(basePath, cwd) : cwd ?? process.cwd();
1202
1286
  if (!(0, fs_1.existsSync)(base)) {
1203
- return { success: false, output: '', error: `Path not found: ${basePath}` };
1287
+ return { success: false, output: '', error: `Path not found: ${normalizedBasePath}` };
1204
1288
  }
1205
1289
  if (!(0, fs_1.statSync)(base).isDirectory()) {
1206
- return { success: false, output: '', error: `Path is not a directory: ${basePath}` };
1290
+ return { success: false, output: '', error: `Path is not a directory: ${normalizedBasePath}` };
1207
1291
  }
1208
1292
  const results = [];
1209
1293
  // Convert glob pattern to regex
@@ -1270,11 +1354,12 @@ async function glob_(pattern, basePath) {
1270
1354
  /**
1271
1355
  * Grep 搜索 - 在文件中搜索正则表达式
1272
1356
  */
1273
- async function grep_(pattern, basePath, globPattern, contextLines) {
1357
+ async function grep_(pattern, basePath, globPattern, contextLines, cwd) {
1274
1358
  try {
1275
- const base = basePath ? safePath(basePath) : process.cwd();
1359
+ const normalizedBasePath = basePath ? normalizeToolPath(basePath) : cwd ?? process.cwd();
1360
+ const base = basePath ? safePath(basePath, cwd) : cwd ?? process.cwd();
1276
1361
  if (!(0, fs_1.existsSync)(base)) {
1277
- return { success: false, output: '', error: `Path not found: ${basePath}` };
1362
+ return { success: false, output: '', error: `Path not found: ${normalizedBasePath}` };
1278
1363
  }
1279
1364
  const regex = new RegExp(pattern);
1280
1365
  const context = contextLines ?? 0;
@@ -1368,6 +1453,137 @@ async function grep_(pattern, basePath, globPattern, contextLines) {
1368
1453
  return { success: false, output: '', error: String(err.message) };
1369
1454
  }
1370
1455
  }
1456
+ function isRecord(value) {
1457
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
1458
+ }
1459
+ function parseBatchReadSteps(rawSteps) {
1460
+ let value = rawSteps;
1461
+ if (typeof value === 'string') {
1462
+ try {
1463
+ value = JSON.parse(value);
1464
+ }
1465
+ catch {
1466
+ return { error: 'batch_read steps must be an array or a valid JSON array string' };
1467
+ }
1468
+ }
1469
+ if (!Array.isArray(value)) {
1470
+ return { error: 'batch_read requires steps to be an array' };
1471
+ }
1472
+ if (value.length === 0) {
1473
+ return { error: 'batch_read requires at least one step' };
1474
+ }
1475
+ if (value.length > BATCH_READ_MAX_STEPS) {
1476
+ return { error: `batch_read supports at most ${BATCH_READ_MAX_STEPS} steps` };
1477
+ }
1478
+ const steps = [];
1479
+ for (let i = 0; i < value.length; i++) {
1480
+ const step = value[i];
1481
+ if (!isRecord(step)) {
1482
+ return { error: `batch_read step ${i + 1} must be an object` };
1483
+ }
1484
+ if (typeof step.tool !== 'string' || !step.tool) {
1485
+ return { error: `batch_read step ${i + 1} requires a tool string` };
1486
+ }
1487
+ let stepArgs = step.args;
1488
+ if (typeof stepArgs === 'string') {
1489
+ try {
1490
+ stepArgs = JSON.parse(stepArgs);
1491
+ }
1492
+ catch {
1493
+ return { error: `batch_read step ${i + 1} args must be an object or valid JSON object string` };
1494
+ }
1495
+ }
1496
+ if (!isRecord(stepArgs)) {
1497
+ return { error: `batch_read step ${i + 1} requires args to be an object` };
1498
+ }
1499
+ steps.push({ tool: step.tool, args: stepArgs });
1500
+ }
1501
+ return { steps };
1502
+ }
1503
+ function buildBatchReadPayload(success, summary, steps, error) {
1504
+ const payload = {
1505
+ success,
1506
+ output: steps.map(step => `${step.index}. ${step.tool}: ${step.summary || (step.success ? 'ok' : step.error || 'error')}`).join('\n'),
1507
+ summary,
1508
+ steps,
1509
+ };
1510
+ if (error) {
1511
+ payload.error = error;
1512
+ }
1513
+ return {
1514
+ success,
1515
+ output: JSON.stringify(payload, null, 2),
1516
+ summary,
1517
+ error,
1518
+ };
1519
+ }
1520
+ async function executeBatchRead(args, context) {
1521
+ const parsed = parseBatchReadSteps(args.steps);
1522
+ if (parsed.error || !parsed.steps) {
1523
+ return buildBatchReadPayload(false, parsed.error || 'Invalid batch_read request', [], parsed.error);
1524
+ }
1525
+ const runtimeTools = getRuntimeTools();
1526
+ for (let i = 0; i < parsed.steps.length; i++) {
1527
+ const step = parsed.steps[i];
1528
+ const tool = runtimeTools.find(t => t.name === step.tool);
1529
+ if (!BATCH_READ_ALLOWED_TOOLS.has(step.tool)) {
1530
+ const error = `Tool ${step.tool} is not allowed in batch_read`;
1531
+ return buildBatchReadPayload(false, error, [{
1532
+ index: i + 1,
1533
+ tool: step.tool,
1534
+ args: step.args,
1535
+ success: false,
1536
+ error,
1537
+ output: '',
1538
+ }], error);
1539
+ }
1540
+ if (!tool || tool.isReadOnly?.(step.args) !== true) {
1541
+ const error = `Tool ${step.tool} is unavailable or not read-only`;
1542
+ return buildBatchReadPayload(false, error, [{
1543
+ index: i + 1,
1544
+ tool: step.tool,
1545
+ args: step.args,
1546
+ success: false,
1547
+ error,
1548
+ output: '',
1549
+ }], error);
1550
+ }
1551
+ }
1552
+ const stepResults = [];
1553
+ for (let i = 0; i < parsed.steps.length; i++) {
1554
+ const step = parsed.steps[i];
1555
+ try {
1556
+ const rawResult = await executeTool(step.tool, step.args, context.abortSignal, context);
1557
+ const envelope = JSON.parse(rawResult);
1558
+ const output = typeof envelope.output === 'string'
1559
+ ? envelope.output
1560
+ : JSON.stringify(envelope.output ?? '');
1561
+ stepResults.push({
1562
+ index: i + 1,
1563
+ tool: step.tool,
1564
+ args: step.args,
1565
+ success: envelope.success === true,
1566
+ summary: typeof envelope.summary === 'string' ? envelope.summary : undefined,
1567
+ error: typeof envelope.error === 'string' ? envelope.error : undefined,
1568
+ output: (0, tool_artifacts_1.truncateForContext)(output, BATCH_READ_STEP_OUTPUT_MAX_BYTES),
1569
+ });
1570
+ }
1571
+ catch (err) {
1572
+ stepResults.push({
1573
+ index: i + 1,
1574
+ tool: step.tool,
1575
+ args: step.args,
1576
+ success: false,
1577
+ error: err?.message || String(err),
1578
+ output: '',
1579
+ });
1580
+ }
1581
+ }
1582
+ const okCount = stepResults.filter(step => step.success).length;
1583
+ const success = okCount === stepResults.length;
1584
+ const summary = `batch_read completed ${okCount}/${stepResults.length} steps`;
1585
+ return buildBatchReadPayload(success, summary, stepResults, success ? undefined : summary);
1586
+ }
1371
1587
  // ============================================================================
1372
1588
  // 统一执行入口
1373
1589
  // ============================================================================