@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.
- package/README.md +1 -1
- package/dist/agents/jarvis.md +1 -1
- package/dist/commands/init.js +4 -4
- package/dist/components/AnimatedStatusText.d.ts +10 -0
- package/dist/components/AnimatedStatusText.js +17 -0
- package/dist/components/ComposerPane.d.ts +25 -0
- package/dist/components/ComposerPane.js +10 -0
- package/dist/components/FooterPane.d.ts +9 -0
- package/dist/components/FooterPane.js +22 -0
- package/dist/components/InputTextView.d.ts +11 -0
- package/dist/components/InputTextView.js +44 -0
- package/dist/components/MarkdownText.d.ts +4 -0
- package/dist/components/MarkdownText.js +10 -3
- package/dist/components/MessageItem.js +4 -1
- package/dist/components/MessageList.d.ts +9 -0
- package/dist/components/MessageList.js +8 -0
- package/dist/components/MessageViewport.d.ts +21 -0
- package/dist/components/MessageViewport.js +11 -0
- package/dist/components/MultilineInput.js +62 -344
- package/dist/components/StatusBar.js +9 -6
- package/dist/components/StreamingDraft.d.ts +11 -0
- package/dist/components/StreamingDraft.js +14 -0
- package/dist/components/WelcomeHeader.js +4 -2
- package/dist/components/inputEditing.d.ts +20 -0
- package/dist/components/inputEditing.js +48 -0
- package/dist/components/setup/SetupConfirmStep.d.ts +8 -0
- package/dist/components/setup/SetupConfirmStep.js +12 -0
- package/dist/components/setup/SetupDoneStep.d.ts +7 -0
- package/dist/components/setup/SetupDoneStep.js +5 -0
- package/dist/components/setup/SetupFormStep.d.ts +11 -0
- package/dist/components/setup/SetupFormStep.js +44 -0
- package/dist/components/setup/SetupHeader.d.ts +9 -0
- package/dist/components/setup/SetupHeader.js +25 -0
- package/dist/components/setup/SetupProviderStep.d.ts +6 -0
- package/dist/components/setup/SetupProviderStep.js +20 -0
- package/dist/components/setup/SetupWelcomeStep.d.ts +5 -0
- package/dist/components/setup/SetupWelcomeStep.js +5 -0
- package/dist/config/bootstrap.d.ts +38 -0
- package/dist/config/bootstrap.js +155 -0
- package/dist/config/constants.d.ts +7 -6
- package/dist/config/constants.js +29 -16
- package/dist/config/loader.d.ts +2 -0
- package/dist/config/loader.js +4 -0
- package/dist/core/hint.js +3 -3
- package/dist/core/query.js +3 -2
- package/dist/hooks/useMultilineInputStream.d.ts +17 -0
- package/dist/hooks/useMultilineInputStream.js +141 -0
- package/dist/hooks/useTerminalCursorSync.d.ts +8 -0
- package/dist/hooks/useTerminalCursorSync.js +44 -0
- package/dist/hooks/useTerminalSize.d.ts +7 -0
- package/dist/hooks/useTerminalSize.js +21 -0
- package/dist/index.js +2 -2
- package/dist/screens/AppBootstrap.d.ts +1 -0
- package/dist/screens/AppBootstrap.js +14 -0
- package/dist/screens/repl.js +39 -28
- package/dist/screens/setup/SetupWizard.d.ts +7 -0
- package/dist/screens/setup/SetupWizard.js +198 -0
- package/dist/services/api/llm.js +5 -3
- package/dist/skills/index.js +10 -3
- package/dist/terminal/cursor.d.ts +6 -0
- package/dist/terminal/cursor.js +21 -0
- package/dist/tools/createSkill.js +59 -1
- package/dist/tools/readFile.js +28 -3
- package/dist/tools/writeFile.js +63 -2
- package/package.json +1 -1
package/dist/skills/index.js
CHANGED
|
@@ -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
|
-
|
|
116
|
-
`
|
|
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 =
|
|
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
|
}
|
package/dist/tools/readFile.js
CHANGED
|
@@ -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: '
|
|
9
|
+
path: { type: 'string', description: '文件路径(支持绝对路径或相对于当前工作目录的相对路径)', required: true },
|
|
7
10
|
},
|
|
8
11
|
execute: async (args) => {
|
|
9
|
-
const
|
|
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
|
-
|
|
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
|
};
|
package/dist/tools/writeFile.js
CHANGED
|
@@ -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: '
|
|
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) {
|