@code4bug/jarvis-agent 1.2.1 → 1.3.2

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 (35) hide show
  1. package/README.md +1 -1
  2. package/dist/agents/jarvis.md +1 -1
  3. package/dist/components/AnimatedStatusText.d.ts +10 -0
  4. package/dist/components/AnimatedStatusText.js +17 -0
  5. package/dist/components/ComposerPane.d.ts +25 -0
  6. package/dist/components/ComposerPane.js +10 -0
  7. package/dist/components/FooterPane.d.ts +9 -0
  8. package/dist/components/FooterPane.js +22 -0
  9. package/dist/components/InputTextView.d.ts +11 -0
  10. package/dist/components/InputTextView.js +44 -0
  11. package/dist/components/MessageList.d.ts +9 -0
  12. package/dist/components/MessageList.js +8 -0
  13. package/dist/components/MessageViewport.d.ts +21 -0
  14. package/dist/components/MessageViewport.js +11 -0
  15. package/dist/components/MultilineInput.js +75 -343
  16. package/dist/components/StreamingDraft.d.ts +11 -0
  17. package/dist/components/StreamingDraft.js +14 -0
  18. package/dist/components/inputEditing.d.ts +20 -0
  19. package/dist/components/inputEditing.js +48 -0
  20. package/dist/core/WorkerBridge.d.ts +3 -0
  21. package/dist/core/WorkerBridge.js +75 -16
  22. package/dist/core/query.js +68 -10
  23. package/dist/hooks/useMultilineInputStream.d.ts +17 -0
  24. package/dist/hooks/useMultilineInputStream.js +141 -0
  25. package/dist/hooks/useTerminalCursorSync.d.ts +11 -0
  26. package/dist/hooks/useTerminalCursorSync.js +46 -0
  27. package/dist/hooks/useTerminalSize.d.ts +7 -0
  28. package/dist/hooks/useTerminalSize.js +21 -0
  29. package/dist/screens/repl.js +74 -33
  30. package/dist/services/api/llm.js +3 -1
  31. package/dist/skills/index.js +10 -3
  32. package/dist/terminal/cursor.d.ts +6 -0
  33. package/dist/terminal/cursor.js +22 -0
  34. package/dist/tools/writeFile.js +63 -2
  35. package/package.json +1 -1
@@ -1,20 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useCallback, useRef, useEffect } from 'react';
3
- import { Box, Text, useInput, useApp } from 'ink';
4
- import Spinner from 'ink-spinner';
5
- import MultilineInput from '../components/MultilineInput.js';
3
+ import { Box, useInput, useApp } from 'ink';
6
4
  import WelcomeHeader from '../components/WelcomeHeader.js';
7
- import MessageItem from '../components/MessageItem.js';
8
- import StreamingText from '../components/StreamingText.js';
9
- import StatusBar from '../components/StatusBar.js';
10
- import SlashCommandMenu from '../components/SlashCommandMenu.js';
11
- import DangerConfirm from '../components/DangerConfirm.js';
5
+ import MessageViewport from '../components/MessageViewport.js';
6
+ import ComposerPane from '../components/ComposerPane.js';
7
+ import FooterPane from '../components/FooterPane.js';
12
8
  import { useWindowFocus } from '../hooks/useFocus.js';
13
9
  import { useInputHistory } from '../hooks/useInputHistory.js';
14
10
  import { useDoubleCtrlCExit } from '../hooks/useDoubleCtrlCExit.js';
15
11
  import { useTerminalWidth } from '../hooks/useTerminalWidth.js';
16
12
  import { useStreamThrottle } from '../hooks/useStreamThrottle.js';
17
- import { useTokenDisplay } from '../hooks/useTokenDisplay.js';
18
13
  import { useSlashMenu } from '../hooks/useSlashMenu.js';
19
14
  import { executeSlashCommand } from './slashCommands.js';
20
15
  import { QueryEngine } from '../core/QueryEngine.js';
@@ -24,6 +19,7 @@ import { subscribeAgentCount, getActiveAgentCount } from '../core/spawnRegistry.
24
19
  import { logError, logInfo, logWarn } from '../core/logger.js';
25
20
  import { getAgentSubCommands } from '../commands/index.js';
26
21
  import { setActiveAgent } from '../config/agentState.js';
22
+ import { hideTerminalCursor, showTerminalCursor } from '../terminal/cursor.js';
27
23
  export default function REPL() {
28
24
  const { exit } = useApp();
29
25
  const width = useTerminalWidth();
@@ -43,13 +39,63 @@ export default function REPL() {
43
39
  const [placeholder, setPlaceholder] = useState('');
44
40
  const [activeAgents, setActiveAgents] = useState(getActiveAgentCount());
45
41
  const lastEscRef = useRef(0);
42
+ const abortRequestedRef = useRef(false);
43
+ const lastAbortNoticeRef = useRef(0);
46
44
  const sessionRef = useRef({
47
45
  id: '', messages: [], createdAt: 0, updatedAt: 0, totalTokens: 0, totalCost: 0,
48
46
  });
49
47
  const engineRef = useRef(null);
48
+ const tokenCountRef = useRef(0);
50
49
  // 节流 hooks
51
50
  const { streamText, streamBufferRef, startStreamTimer, stopStreamTimer, appendStreamChunk, clearStream, handleThinkingUpdate, finishThinking, thinkingIdRef, stopAll, } = useStreamThrottle(setMessages);
52
- const { displayTokens, tokenCountRef, startTokenTimer, stopTokenTimer, updateTokenCount, syncTokenDisplay, resetTokens, } = useTokenDisplay();
51
+ const syncTokenDisplay = useCallback((count) => {
52
+ tokenCountRef.current = count;
53
+ }, []);
54
+ const resetTokens = useCallback(() => {
55
+ tokenCountRef.current = 0;
56
+ }, []);
57
+ const appendAbortNotice = useCallback(() => {
58
+ const now = Date.now();
59
+ if (now - lastAbortNoticeRef.current < 800)
60
+ return;
61
+ lastAbortNoticeRef.current = now;
62
+ setMessages((prev) => [...prev, {
63
+ id: `abort-notice-${now}`,
64
+ type: 'system',
65
+ status: 'aborted',
66
+ content: '已中断当前推理',
67
+ timestamp: now,
68
+ abortHint: '推理已中断(ESC)',
69
+ }]);
70
+ }, []);
71
+ const markPendingMessagesAborted = useCallback(() => {
72
+ setMessages((prev) => prev
73
+ .filter((msg) => !(msg.type === 'thinking' && msg.status === 'pending'))
74
+ .map((msg) => {
75
+ if (msg.status !== 'pending')
76
+ return msg;
77
+ if (msg.type === 'tool_exec') {
78
+ return {
79
+ ...msg,
80
+ status: 'aborted',
81
+ content: `${msg.toolName || '工具'} 已中断`,
82
+ abortHint: msg.abortHint ?? '命令已中断(ESC)',
83
+ };
84
+ }
85
+ return msg;
86
+ }));
87
+ finishThinking();
88
+ clearStream();
89
+ }, [clearStream, finishThinking]);
90
+ const requestAbort = useCallback(() => {
91
+ if (!isProcessing || !engineRef.current || abortRequestedRef.current)
92
+ return;
93
+ abortRequestedRef.current = true;
94
+ logWarn('ui.abort_by_escape');
95
+ markPendingMessagesAborted();
96
+ appendAbortNotice();
97
+ engineRef.current.abort();
98
+ }, [appendAbortNotice, isProcessing, markPendingMessagesAborted]);
53
99
  // ===== 新会话逻辑 =====
54
100
  const handleNewSession = useCallback(() => {
55
101
  logInfo('ui.new_session');
@@ -61,6 +107,7 @@ export default function REPL() {
61
107
  clearStream();
62
108
  setLoopState(null);
63
109
  setIsProcessing(false);
110
+ abortRequestedRef.current = false;
64
111
  setShowWelcome(true);
65
112
  resetTokens();
66
113
  generateAgentHint().then((hint) => setPlaceholder(hint)).catch(() => { });
@@ -125,17 +172,16 @@ export default function REPL() {
125
172
  setLoopState(state);
126
173
  setIsProcessing(state.isRunning);
127
174
  if (state.isRunning) {
128
- startTokenTimer();
129
175
  startStreamTimer();
130
176
  }
131
177
  else {
132
- stopTokenTimer();
178
+ abortRequestedRef.current = false;
133
179
  stopAll();
134
180
  }
135
181
  },
136
182
  onSessionUpdate: (s) => {
137
183
  sessionRef.current = s;
138
- updateTokenCount(s.totalTokens);
184
+ tokenCountRef.current = s.totalTokens;
139
185
  },
140
186
  onConfirmDangerousCommand: (command, reason, ruleName) => {
141
187
  return new Promise((resolve) => {
@@ -212,6 +258,7 @@ export default function REPL() {
212
258
  setShowWelcome(false);
213
259
  pushHistory(trimmed);
214
260
  clearStream();
261
+ abortRequestedRef.current = false;
215
262
  await engineRef.current.handleQuery(prompt, callbacks);
216
263
  return;
217
264
  }
@@ -270,6 +317,7 @@ export default function REPL() {
270
317
  pushHistory(trimmed);
271
318
  setInput('');
272
319
  clearStream();
320
+ abortRequestedRef.current = false;
273
321
  await engineRef.current.handleQuery(trimmed, callbacks);
274
322
  }, [isProcessing, pushHistory, clearStream, slashMenu, handleNewSession]);
275
323
  // ===== 输入处理 =====
@@ -321,19 +369,11 @@ export default function REPL() {
321
369
  }, [placeholder]);
322
370
  // ===== 隐藏系统默认终端光标,使用 ink 渲染的反色光标代替 =====
323
371
  useEffect(() => {
324
- process.stdout.write('\x1B[?25l');
372
+ hideTerminalCursor();
325
373
  return () => {
326
- process.stdout.write('\x1B[?25h');
374
+ showTerminalCursor();
327
375
  };
328
376
  }, []);
329
- // ===== processing/streaming 期间,每次 re-render 后将物理光标移到行首 =====
330
- // ink 每次渲染后光标停在 StatusBar 行末,macOS IME 会在该位置显示 composing 候选框,
331
- // 导致候选框与进度条重叠。将光标移到列 1(行首)让候选框出现在屏幕左侧空白区域。
332
- useEffect(() => {
333
- if (!isProcessing)
334
- return;
335
- process.stdout.write('\x1B[1G');
336
- });
337
377
  // ===== processing 期间隐藏终端光标,阻止 IME composing 显示 =====
338
378
  const isProcessingRef = useRef(isProcessing);
339
379
  isProcessingRef.current = isProcessing;
@@ -341,7 +381,7 @@ export default function REPL() {
341
381
  if (!isProcessing)
342
382
  return;
343
383
  // 隐藏终端光标 — 终端 IME 的 inline composing 仅在光标可见时渲染
344
- process.stdout.write('\x1B[?25l');
384
+ hideTerminalCursor();
345
385
  // 高优先级 stdin 拦截:吞掉 processing 期间的所有输入(Ctrl+C / ESC 除外)
346
386
  // 防止字符积累在缓冲区,processing 结束后污染输入框
347
387
  const drain = (data) => {
@@ -373,8 +413,8 @@ export default function REPL() {
373
413
  handleCtrlC();
374
414
  return;
375
415
  }
376
- if (key.escape && engineRef.current) {
377
- engineRef.current.abort();
416
+ if (key.escape) {
417
+ requestAbort();
378
418
  return;
379
419
  }
380
420
  if (key.ctrl && ch === 'o') {
@@ -410,9 +450,8 @@ export default function REPL() {
410
450
  return;
411
451
  }
412
452
  if (key.escape) {
413
- if (isProcessing && engineRef.current) {
414
- logWarn('ui.abort_by_escape');
415
- engineRef.current.abort();
453
+ if (isProcessing) {
454
+ requestAbort();
416
455
  }
417
456
  else if (input.length > 0) {
418
457
  const now = Date.now();
@@ -428,8 +467,10 @@ export default function REPL() {
428
467
  }
429
468
  });
430
469
  // ===== 渲染 =====
431
- return (_jsxs(Box, { flexDirection: "column", width: width, children: [showWelcome && _jsx(WelcomeHeader, { width: width }), _jsxs(Box, { flexDirection: "column", paddingX: 1, marginTop: showWelcome ? 0 : 1, children: [messages.map((msg) => (_jsx(MessageItem, { msg: msg, showDetails: showDetails }, msg.id))), streamText && _jsx(StreamingText, { text: streamText }), dangerConfirm && (_jsx(DangerConfirm, { command: dangerConfirm.command, reason: dangerConfirm.reason, ruleName: dangerConfirm.ruleName, onSelect: (choice) => {
432
- dangerConfirm.resolve(choice);
433
- setDangerConfirm(null);
434
- } })), loopState?.isRunning && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { color: "gray", children: [" iteration ", loopState.iteration, "/", loopState.maxIterations] })] }))] }), _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: "gray", children: '─'.repeat(Math.max(width - 2, 1)) }), slashMenu.slashMenuVisible && !isProcessing && (_jsx(SlashCommandMenu, { commands: slashMenu.slashMenuItems, selectedIndex: slashMenu.slashMenuIndex })), _jsx(Box, { children: countdown !== null ? (_jsxs(Box, { children: [_jsx(Text, { color: "gray", dimColor: true, children: "\u276F " }), _jsx(Text, { color: "yellow", children: "Press " }), _jsx(Text, { color: "yellow", bold: true, children: "Ctrl+C" }), _jsx(Text, { color: "yellow", children: " again to exit " }), _jsxs(Text, { color: "gray", dimColor: true, children: ["(", countdown, "s)"] })] })) : isProcessing ? (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: "\u276F " }), _jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { color: "gray", italic: true, children: " processing..." })] })) : (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: "\u276F " }), _jsx(MultilineInput, { value: input, onChange: handleInputChange, onSubmit: handleEditorSubmit, onUpArrow: handleUpArrow, onDownArrow: handleDownArrow, placeholder: placeholder, isActive: !isProcessing, showCursor: windowFocused && !isProcessing, slashMenuActive: slashMenu.slashMenuVisible, onSlashMenuUp: slashMenu.handleSlashMenuUp, onSlashMenuDown: slashMenu.handleSlashMenuDown, onSlashMenuSelect: handleSlashMenuAutocomplete, onSlashMenuClose: slashMenu.handleSlashMenuClose, onTabFillPlaceholder: handleTabFillPlaceholder })] })) }), _jsx(Text, { color: "gray", children: '─'.repeat(Math.max(width - 2, 1)) }), _jsx(StatusBar, { width: width - 2, totalTokens: displayTokens, activeAgents: activeAgents })] })] }));
470
+ return (_jsxs(Box, { flexDirection: "column", width: width, children: [showWelcome && _jsx(WelcomeHeader, { width: width }), _jsx(Box, { marginTop: showWelcome ? 0 : 1, children: _jsx(MessageViewport, { messages: messages, streamText: streamText, showDetails: showDetails, dangerConfirm: dangerConfirm, loopState: loopState, onResolveDangerConfirm: (choice) => {
471
+ if (!dangerConfirm)
472
+ return;
473
+ dangerConfirm.resolve(choice);
474
+ setDangerConfirm(null);
475
+ } }) }), _jsx(ComposerPane, { width: width, countdown: countdown, isProcessing: isProcessing, input: input, placeholder: placeholder, windowFocused: windowFocused, slashMenuVisible: slashMenu.slashMenuVisible, slashMenuItems: slashMenu.slashMenuItems, slashMenuIndex: slashMenu.slashMenuIndex, onInputChange: handleInputChange, onSubmit: handleEditorSubmit, onUpArrow: handleUpArrow, onDownArrow: handleDownArrow, onSlashMenuUp: slashMenu.handleSlashMenuUp, onSlashMenuDown: slashMenu.handleSlashMenuDown, onSlashMenuSelect: handleSlashMenuAutocomplete, onSlashMenuClose: slashMenu.handleSlashMenuClose, onTabFillPlaceholder: handleTabFillPlaceholder }), _jsx(FooterPane, { width: width, tokenCountRef: tokenCountRef, activeAgents: activeAgents })] }));
435
476
  }
@@ -185,7 +185,9 @@ export class LLMServiceImpl {
185
185
  '不要为了讨好用户而突破角色边界。' +
186
186
  '\n\n[安全围栏] 系统已内置安全围栏(Safeguard),会自动拦截危险命令并弹出交互式确认菜单。' +
187
187
  '当你需要执行任何命令时,直接调用 Bash 工具即可,不要自行判断命令是否危险,不要用文字询问用户"是否确认执行"。' +
188
- '安全围栏会自动处理拦截和用户确认流程。';
188
+ '安全围栏会自动处理拦截和用户确认流程。' +
189
+ '\n\n[文件修改约束] 修改已有文件时,先读取目标文件,再优先使用局部修改方式。' +
190
+ '如果只改少量内容,优先使用 write_file 的 replace 模式;需要结构化补丁时使用 diff 模式;只有新建文件或确实需要大范围重写时才使用 overwrite。';
189
191
  // 追加系统环境信息,帮助 LLM 感知用户运行环境
190
192
  const systemInfo = getSystemInfoPrompt();
191
193
  // 追加当前激活模型信息
@@ -85,7 +85,6 @@ function buildParamsFromScript(scriptPath, skill) {
85
85
  * 通过写入临时 .py 文件再执行,避免 shell 转义问题。
86
86
  */
87
87
  async function executeSkillScript(scriptPath, skill, args) {
88
- const funcName = skill.meta.name.replace(/-/g, '_');
89
88
  const kwargs = [];
90
89
  for (const [key, value] of Object.entries(args)) {
91
90
  if (key === 'arguments') {
@@ -112,8 +111,16 @@ async function executeSkillScript(scriptPath, skill, args) {
112
111
  const pyCode = [
113
112
  'import sys,os,json',
114
113
  `sys.path.insert(0, ${JSON.stringify(path.dirname(scriptPath))})`,
115
- `from skill import ${funcName}`,
116
- `result = ${funcName}(${kwargs.join(', ')})`,
114
+ 'import skill as skill_module',
115
+ `tool_name = getattr(skill_module, "TOOL_METADATA", {}).get("name") or ${JSON.stringify(skill.meta.name.replace(/-/g, '_'))}`,
116
+ 'func = getattr(skill_module, tool_name, None)',
117
+ 'if func is None:',
118
+ ' candidates = [name for name, value in vars(skill_module).items() if callable(value) and not name.startswith("_")]',
119
+ ' if len(candidates) == 1:',
120
+ ' func = getattr(skill_module, candidates[0])',
121
+ ' else:',
122
+ ' raise ImportError(f"未找到可调用函数: {tool_name},候选: {candidates}")',
123
+ `result = func(${kwargs.join(', ')})`,
117
124
  'print(json.dumps(result, ensure_ascii=False, indent=2))',
118
125
  ].join('\n');
119
126
  try {
@@ -0,0 +1,6 @@
1
+ export declare function hideTerminalCursor(): void;
2
+ export declare function showTerminalCursor(): void;
3
+ export declare function enableBracketedPaste(): void;
4
+ export declare function disableBracketedPaste(): void;
5
+ export declare function moveCursorToColumn(column: number): void;
6
+ export declare function relocateCursorToInputLine(rowsBelow: number, column: number, extraRowsUp?: number): void;
@@ -0,0 +1,22 @@
1
+ const ESC = '\x1B[';
2
+ export function hideTerminalCursor() {
3
+ process.stdout.write(`${ESC}?25l`);
4
+ }
5
+ export function showTerminalCursor() {
6
+ process.stdout.write(`${ESC}?25h`);
7
+ }
8
+ export function enableBracketedPaste() {
9
+ process.stdout.write(`${ESC}?2004h`);
10
+ }
11
+ export function disableBracketedPaste() {
12
+ process.stdout.write(`${ESC}?2004l`);
13
+ }
14
+ export function moveCursorToColumn(column) {
15
+ process.stdout.write(`${ESC}${column}G`);
16
+ }
17
+ export function relocateCursorToInputLine(rowsBelow, column, extraRowsUp = 0) {
18
+ const totalRowsUp = Math.max(rowsBelow + extraRowsUp, 0);
19
+ const up = totalRowsUp > 0 ? `${ESC}${totalRowsUp}A` : '';
20
+ const down = totalRowsUp > 0 ? `${ESC}${totalRowsUp}B` : '';
21
+ process.stdout.write(`${up}${ESC}${column}G${down}`);
22
+ }
@@ -73,6 +73,35 @@ function applyUnifiedDiff(original, diff) {
73
73
  }
74
74
  return result.join('\n');
75
75
  }
76
+ /**
77
+ * 基于文本片段做精确替换,适合只改少量代码的场景。
78
+ */
79
+ function applyStringReplace(original, oldString, newString, replaceAll, expectedReplacements) {
80
+ if (!oldString) {
81
+ throw new Error('replace 模式下 old_string 不能为空');
82
+ }
83
+ let matchCount = 0;
84
+ let searchStart = 0;
85
+ while (true) {
86
+ const idx = original.indexOf(oldString, searchStart);
87
+ if (idx === -1)
88
+ break;
89
+ matchCount++;
90
+ searchStart = idx + oldString.length;
91
+ }
92
+ if (matchCount === 0) {
93
+ throw new Error('replace 模式未找到要替换的内容');
94
+ }
95
+ if (expectedReplacements !== undefined && matchCount !== expectedReplacements) {
96
+ throw new Error(`replace 模式匹配数量不符合预期,期望 ${expectedReplacements},实际 ${matchCount}`);
97
+ }
98
+ if (!replaceAll && matchCount !== 1) {
99
+ throw new Error(`replace 模式要求目标内容唯一,当前匹配到 ${matchCount} 处。请提供更精确的 old_string,或开启 replace_all`);
100
+ }
101
+ return replaceAll
102
+ ? original.split(oldString).join(newString)
103
+ : original.replace(oldString, newString);
104
+ }
76
105
  /**
77
106
  * 写入文件内容(支持安全检测)。
78
107
  * 返回写入结果消息。
@@ -95,13 +124,33 @@ function doWrite(filePath, content) {
95
124
  }
96
125
  export const writeFile = {
97
126
  name: 'write_file',
98
- description: '写入内容到指定文件。支持两种模式:overwrite(默认)完整替换文件内容;diff 模式接收 unified diff 格式补丁,对文件进行增量更新。',
127
+ description: '写入内容到指定文件。修改已有文件时优先使用 replace 或 diff 做局部更新,只有新建文件或大范围重写时才使用 overwrite。支持三种模式:overwrite(完整替换)、replace(按文本片段精确替换)、diff(基于 unified diff 增量更新)。',
99
128
  parameters: {
100
129
  path: { type: 'string', description: '文件路径', required: true },
101
130
  content: { type: 'string', description: '文件内容(overwrite 模式必填)', required: false },
102
131
  mode: {
103
132
  type: 'string',
104
- description: '写入模式:overwrite(完整替换,默认)| diff(增量更新)',
133
+ description: '写入模式:overwrite(完整替换,默认)| replace(局部替换)| diff(增量更新)',
134
+ required: false,
135
+ },
136
+ old_string: {
137
+ type: 'string',
138
+ description: 'replace 模式必填:要查找并替换的原始文本片段',
139
+ required: false,
140
+ },
141
+ new_string: {
142
+ type: 'string',
143
+ description: 'replace 模式必填:替换后的文本片段',
144
+ required: false,
145
+ },
146
+ replace_all: {
147
+ type: 'boolean',
148
+ description: 'replace 模式可选:是否替换所有匹配项,默认 false',
149
+ required: false,
150
+ },
151
+ expected_replacements: {
152
+ type: 'number',
153
+ description: 'replace 模式可选:期望匹配数量,不符合时直接报错,防止误替换',
105
154
  required: false,
106
155
  },
107
156
  diff: {
@@ -113,6 +162,18 @@ export const writeFile = {
113
162
  execute: async (args) => {
114
163
  const filePath = args.path;
115
164
  const mode = args.mode || 'overwrite';
165
+ if (mode === 'replace') {
166
+ const oldString = args.old_string;
167
+ const newString = args.new_string ?? '';
168
+ const replaceAll = Boolean(args.replace_all);
169
+ const expectedReplacements = args.expected_replacements === undefined ? undefined : Number(args.expected_replacements);
170
+ if (!fs.existsSync(filePath)) {
171
+ throw new Error(`replace 模式要求目标文件已存在: ${filePath}`);
172
+ }
173
+ const original = fs.readFileSync(filePath, 'utf-8');
174
+ const replaced = applyStringReplace(original, oldString, newString, replaceAll, expectedReplacements);
175
+ return doWrite(filePath, replaced);
176
+ }
116
177
  if (mode === 'diff') {
117
178
  const diffContent = args.diff;
118
179
  if (!diffContent) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code4bug/jarvis-agent",
3
- "version": "1.2.1",
3
+ "version": "1.3.2",
4
4
  "description": "基于 React + TypeScript + Ink 构建的命令行智能体交互界面",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",