@code4bug/jarvis-agent 1.1.8 → 1.3.1

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 (65) hide show
  1. package/README.md +1 -1
  2. package/dist/agents/jarvis.md +1 -1
  3. package/dist/commands/init.js +4 -4
  4. package/dist/components/AnimatedStatusText.d.ts +10 -0
  5. package/dist/components/AnimatedStatusText.js +17 -0
  6. package/dist/components/ComposerPane.d.ts +25 -0
  7. package/dist/components/ComposerPane.js +10 -0
  8. package/dist/components/FooterPane.d.ts +9 -0
  9. package/dist/components/FooterPane.js +22 -0
  10. package/dist/components/InputTextView.d.ts +11 -0
  11. package/dist/components/InputTextView.js +44 -0
  12. package/dist/components/MarkdownText.d.ts +4 -0
  13. package/dist/components/MarkdownText.js +10 -3
  14. package/dist/components/MessageItem.js +4 -1
  15. package/dist/components/MessageList.d.ts +9 -0
  16. package/dist/components/MessageList.js +8 -0
  17. package/dist/components/MessageViewport.d.ts +21 -0
  18. package/dist/components/MessageViewport.js +11 -0
  19. package/dist/components/MultilineInput.js +62 -344
  20. package/dist/components/StatusBar.js +9 -6
  21. package/dist/components/StreamingDraft.d.ts +11 -0
  22. package/dist/components/StreamingDraft.js +14 -0
  23. package/dist/components/WelcomeHeader.js +4 -2
  24. package/dist/components/inputEditing.d.ts +20 -0
  25. package/dist/components/inputEditing.js +48 -0
  26. package/dist/components/setup/SetupConfirmStep.d.ts +8 -0
  27. package/dist/components/setup/SetupConfirmStep.js +12 -0
  28. package/dist/components/setup/SetupDoneStep.d.ts +7 -0
  29. package/dist/components/setup/SetupDoneStep.js +5 -0
  30. package/dist/components/setup/SetupFormStep.d.ts +11 -0
  31. package/dist/components/setup/SetupFormStep.js +44 -0
  32. package/dist/components/setup/SetupHeader.d.ts +9 -0
  33. package/dist/components/setup/SetupHeader.js +25 -0
  34. package/dist/components/setup/SetupProviderStep.d.ts +6 -0
  35. package/dist/components/setup/SetupProviderStep.js +20 -0
  36. package/dist/components/setup/SetupWelcomeStep.d.ts +5 -0
  37. package/dist/components/setup/SetupWelcomeStep.js +5 -0
  38. package/dist/config/bootstrap.d.ts +38 -0
  39. package/dist/config/bootstrap.js +155 -0
  40. package/dist/config/constants.d.ts +7 -6
  41. package/dist/config/constants.js +29 -16
  42. package/dist/config/loader.d.ts +2 -0
  43. package/dist/config/loader.js +4 -0
  44. package/dist/core/hint.js +3 -3
  45. package/dist/core/query.js +3 -2
  46. package/dist/hooks/useMultilineInputStream.d.ts +17 -0
  47. package/dist/hooks/useMultilineInputStream.js +141 -0
  48. package/dist/hooks/useTerminalCursorSync.d.ts +8 -0
  49. package/dist/hooks/useTerminalCursorSync.js +44 -0
  50. package/dist/hooks/useTerminalSize.d.ts +7 -0
  51. package/dist/hooks/useTerminalSize.js +21 -0
  52. package/dist/index.js +2 -2
  53. package/dist/screens/AppBootstrap.d.ts +1 -0
  54. package/dist/screens/AppBootstrap.js +14 -0
  55. package/dist/screens/repl.js +39 -28
  56. package/dist/screens/setup/SetupWizard.d.ts +7 -0
  57. package/dist/screens/setup/SetupWizard.js +198 -0
  58. package/dist/services/api/llm.js +5 -3
  59. package/dist/skills/index.js +10 -3
  60. package/dist/terminal/cursor.d.ts +6 -0
  61. package/dist/terminal/cursor.js +21 -0
  62. package/dist/tools/createSkill.js +59 -1
  63. package/dist/tools/readFile.js +28 -3
  64. package/dist/tools/writeFile.js +63 -2
  65. package/package.json +1 -1
@@ -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): void;
@@ -0,0 +1,21 @@
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) {
18
+ const up = rowsBelow > 0 ? `${ESC}${rowsBelow}A` : '';
19
+ const down = rowsBelow > 0 ? `${ESC}${rowsBelow}B` : '';
20
+ process.stdout.write(`${up}${ESC}${column}G${down}`);
21
+ }
@@ -96,6 +96,64 @@ function ensureFrontmatterDescription(skillMd, description) {
96
96
  // 兜底:在 frontmatter 末尾插入
97
97
  return skillMd.replace(/\n---/, `\ndescription: ${description}\n---`);
98
98
  }
99
+ function decodeLooseJsonString(value) {
100
+ return value
101
+ .replace(/\\"/g, '"')
102
+ .replace(/\\n/g, '\n')
103
+ .replace(/\\r/g, '\r')
104
+ .replace(/\\t/g, '\t')
105
+ .replace(/\\\\/g, '\\');
106
+ }
107
+ function escapeRegExp(value) {
108
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
109
+ }
110
+ function extractLooseField(jsonStr, key, nextKeys) {
111
+ const keyPattern = new RegExp(`"${escapeRegExp(key)}"\\s*:\\s*"`, 'm');
112
+ const keyMatch = keyPattern.exec(jsonStr);
113
+ if (!keyMatch)
114
+ return undefined;
115
+ const valueStart = keyMatch.index + keyMatch[0].length;
116
+ let valueEnd = jsonStr.length;
117
+ for (const nextKey of nextKeys) {
118
+ const nextPattern = new RegExp(`"\\s*,\\s*\\n\\s*"${escapeRegExp(nextKey)}"\\s*:`, 'm');
119
+ const tail = jsonStr.slice(valueStart);
120
+ const nextMatch = nextPattern.exec(tail);
121
+ if (nextMatch) {
122
+ valueEnd = Math.min(valueEnd, valueStart + nextMatch.index);
123
+ }
124
+ }
125
+ if (nextKeys.length === 0) {
126
+ const tail = jsonStr.slice(valueStart);
127
+ const tailMatch = /"\s*\n?\s*}/m.exec(tail);
128
+ if (tailMatch) {
129
+ valueEnd = valueStart + tailMatch.index;
130
+ }
131
+ }
132
+ const rawValue = jsonStr.slice(valueStart, valueEnd);
133
+ return decodeLooseJsonString(rawValue);
134
+ }
135
+ function parseSkillJsonResult(jsonStr) {
136
+ try {
137
+ return JSON.parse(jsonStr);
138
+ }
139
+ catch {
140
+ const name = extractLooseField(jsonStr, 'name', ['description', 'argument_hint', 'skill_md', 'skill_py']);
141
+ const description = extractLooseField(jsonStr, 'description', ['argument_hint', 'skill_md', 'skill_py']);
142
+ const argument_hint = extractLooseField(jsonStr, 'argument_hint', ['skill_md', 'skill_py']);
143
+ const skill_md = extractLooseField(jsonStr, 'skill_md', ['skill_py']);
144
+ const skill_py = extractLooseField(jsonStr, 'skill_py', []);
145
+ if (!name || !description || !skill_md) {
146
+ throw new Error('无法从 LLM 输出中提取必要字段 (name, description, skill_md)');
147
+ }
148
+ return {
149
+ name: name.trim(),
150
+ description: description.trim(),
151
+ argument_hint: argument_hint?.trim() || undefined,
152
+ skill_md,
153
+ skill_py: skill_py || undefined,
154
+ };
155
+ }
156
+ }
99
157
  /**
100
158
  * 调用 LLM 生成 skill 内容
101
159
  *
@@ -179,7 +237,7 @@ async function callLLMForSkill(requirement) {
179
237
  jsonStr = jsonBlockMatch[1].trim();
180
238
  }
181
239
  try {
182
- const parsed = JSON.parse(jsonStr);
240
+ const parsed = parseSkillJsonResult(jsonStr);
183
241
  if (!parsed.name || !parsed.skill_md) {
184
242
  throw new Error('LLM 返回的 JSON 缺少必要字段 (name, skill_md)');
185
243
  }
@@ -1,17 +1,42 @@
1
1
  import fs from 'fs';
2
+ import path from 'path';
3
+ // 单次读取文件大小上限:1MB
4
+ const MAX_FILE_SIZE = 1 * 1024 * 1024;
2
5
  export const readFile = {
3
6
  name: 'read_file',
4
7
  description: '读取指定路径的文件内容',
5
8
  parameters: {
6
- path: { type: 'string', description: '文件路径', required: true },
9
+ path: { type: 'string', description: '文件路径(支持绝对路径或相对于当前工作目录的相对路径)', required: true },
7
10
  },
8
11
  execute: async (args) => {
9
- const filePath = args.path;
12
+ const rawPath = args.path;
13
+ if (!rawPath || !rawPath.trim()) {
14
+ throw new Error('path 参数不能为空');
15
+ }
16
+ // 统一解析为绝对路径,相对路径基于 process.cwd()
17
+ const filePath = path.isAbsolute(rawPath) ? rawPath : path.resolve(process.cwd(), rawPath);
10
18
  try {
19
+ const stat = fs.statSync(filePath);
20
+ if (stat.isDirectory()) {
21
+ throw new Error(`路径是目录而非文件: ${filePath}`);
22
+ }
23
+ if (stat.size > MAX_FILE_SIZE) {
24
+ throw new Error(`文件过大(${(stat.size / 1024).toFixed(1)} KB),超过读取上限 ${MAX_FILE_SIZE / 1024} KB,请使用 search_files 或分段读取`);
25
+ }
11
26
  return fs.readFileSync(filePath, 'utf-8');
12
27
  }
13
28
  catch (e) {
14
- throw new Error(`读取文件失败: ${e.message}`);
29
+ // 区分"文件不存在"和其他 IO 错误,给出更明确的提示
30
+ if (e.code === 'ENOENT') {
31
+ throw new Error(`文件不存在: ${filePath}`);
32
+ }
33
+ if (e.code === 'EACCES') {
34
+ throw new Error(`无权限读取文件: ${filePath}`);
35
+ }
36
+ // 重新抛出已包装的错误(如上面的 size/dir 检查)
37
+ throw e.message?.startsWith('文件') || e.message?.startsWith('路径')
38
+ ? e
39
+ : new Error(`读取文件失败: ${e.message}`);
15
40
  }
16
41
  },
17
42
  };
@@ -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.1.8",
3
+ "version": "1.3.1",
4
4
  "description": "基于 React + TypeScript + Ink 构建的命令行智能体交互界面",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",