@code4bug/jarvis-agent 1.0.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 (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +227 -0
  3. package/dist/agents/code-reviewer.md +69 -0
  4. package/dist/agents/dba.md +68 -0
  5. package/dist/agents/finance-advisor.md +81 -0
  6. package/dist/agents/index.d.ts +31 -0
  7. package/dist/agents/index.js +86 -0
  8. package/dist/agents/jarvis.md +95 -0
  9. package/dist/agents/stock-trader.md +81 -0
  10. package/dist/cli.d.ts +2 -0
  11. package/dist/cli.js +9 -0
  12. package/dist/commands/index.d.ts +19 -0
  13. package/dist/commands/index.js +79 -0
  14. package/dist/commands/init.d.ts +15 -0
  15. package/dist/commands/init.js +283 -0
  16. package/dist/components/DangerConfirm.d.ts +17 -0
  17. package/dist/components/DangerConfirm.js +50 -0
  18. package/dist/components/MarkdownText.d.ts +12 -0
  19. package/dist/components/MarkdownText.js +166 -0
  20. package/dist/components/MessageItem.d.ts +8 -0
  21. package/dist/components/MessageItem.js +78 -0
  22. package/dist/components/MultilineInput.d.ts +34 -0
  23. package/dist/components/MultilineInput.js +437 -0
  24. package/dist/components/SlashCommandMenu.d.ts +16 -0
  25. package/dist/components/SlashCommandMenu.js +43 -0
  26. package/dist/components/StatusBar.d.ts +8 -0
  27. package/dist/components/StatusBar.js +26 -0
  28. package/dist/components/StreamingText.d.ts +6 -0
  29. package/dist/components/StreamingText.js +10 -0
  30. package/dist/components/WelcomeHeader.d.ts +6 -0
  31. package/dist/components/WelcomeHeader.js +25 -0
  32. package/dist/config/agentState.d.ts +16 -0
  33. package/dist/config/agentState.js +65 -0
  34. package/dist/config/constants.d.ts +25 -0
  35. package/dist/config/constants.js +67 -0
  36. package/dist/config/loader.d.ts +30 -0
  37. package/dist/config/loader.js +64 -0
  38. package/dist/config/systemInfo.d.ts +12 -0
  39. package/dist/config/systemInfo.js +95 -0
  40. package/dist/core/QueryEngine.d.ts +52 -0
  41. package/dist/core/QueryEngine.js +246 -0
  42. package/dist/core/hint.d.ts +14 -0
  43. package/dist/core/hint.js +279 -0
  44. package/dist/core/query.d.ts +24 -0
  45. package/dist/core/query.js +245 -0
  46. package/dist/core/safeguard.d.ts +96 -0
  47. package/dist/core/safeguard.js +236 -0
  48. package/dist/hooks/useFocus.d.ts +12 -0
  49. package/dist/hooks/useFocus.js +35 -0
  50. package/dist/hooks/useInputHistory.d.ts +14 -0
  51. package/dist/hooks/useInputHistory.js +102 -0
  52. package/dist/index.d.ts +1 -0
  53. package/dist/index.js +6 -0
  54. package/dist/screens/repl.d.ts +1 -0
  55. package/dist/screens/repl.js +842 -0
  56. package/dist/services/api/llm.d.ts +27 -0
  57. package/dist/services/api/llm.js +314 -0
  58. package/dist/services/api/mock.d.ts +9 -0
  59. package/dist/services/api/mock.js +102 -0
  60. package/dist/skills/index.d.ts +23 -0
  61. package/dist/skills/index.js +232 -0
  62. package/dist/skills/loader.d.ts +45 -0
  63. package/dist/skills/loader.js +108 -0
  64. package/dist/tools/createSkill.d.ts +8 -0
  65. package/dist/tools/createSkill.js +255 -0
  66. package/dist/tools/index.d.ts +16 -0
  67. package/dist/tools/index.js +23 -0
  68. package/dist/tools/listDirectory.d.ts +2 -0
  69. package/dist/tools/listDirectory.js +20 -0
  70. package/dist/tools/readFile.d.ts +2 -0
  71. package/dist/tools/readFile.js +17 -0
  72. package/dist/tools/runCommand.d.ts +2 -0
  73. package/dist/tools/runCommand.js +69 -0
  74. package/dist/tools/searchFiles.d.ts +2 -0
  75. package/dist/tools/searchFiles.js +45 -0
  76. package/dist/tools/writeFile.d.ts +2 -0
  77. package/dist/tools/writeFile.js +42 -0
  78. package/dist/types/index.d.ts +86 -0
  79. package/dist/types/index.js +2 -0
  80. package/package.json +55 -0
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Skill 注册中心
3
+ *
4
+ * 负责:
5
+ * 1. 启动时扫描 ~/.jarvis/skills/ 加载外部 skill
6
+ * 2. 将 skill 转换为 Tool 格式,与系统内置 tools 合并
7
+ * 3. 提供统一的工具查找接口
8
+ *
9
+ * 如果 skill 目录下存在 skill.py,execute 时直接调用 Python 脚本获取真实结果;
10
+ * 否则回退为返回 skill 指令文本(由 LLM 解释执行)。
11
+ */
12
+ import { exec } from 'child_process';
13
+ import fs from 'fs';
14
+ import path from 'path';
15
+ import { scanExternalSkills, getExternalSkillsDir } from './loader.js';
16
+ import { allTools as builtinTools } from '../tools/index.js';
17
+ // ===== 缓存 =====
18
+ let _skillCache = null;
19
+ let _mergedTools = null;
20
+ // ===== Skill → Tool 转换 =====
21
+ /**
22
+ * 从 skill.py 的 TOOL_METADATA 中提取参数定义
23
+ */
24
+ function buildParamsFromScript(scriptPath, skill) {
25
+ try {
26
+ const content = fs.readFileSync(scriptPath, 'utf-8');
27
+ const metaStart = content.indexOf('TOOL_METADATA');
28
+ if (metaStart === -1)
29
+ throw new Error('no metadata');
30
+ const metaSection = content.slice(metaStart);
31
+ const paramsStart = metaSection.indexOf('"parameters"');
32
+ if (paramsStart === -1)
33
+ throw new Error('no parameters');
34
+ // 跳过 "parameters": { 本身,从其内部开始匹配参数
35
+ const afterParamsKey = metaSection.slice(paramsStart + '"parameters"'.length);
36
+ const braceIdx = afterParamsKey.indexOf('{');
37
+ if (braceIdx === -1)
38
+ throw new Error('no parameters brace');
39
+ const paramsBody = afterParamsKey.slice(braceIdx + 1);
40
+ // 如果是嵌套的 JSON Schema 格式(含 "properties"),跳到 properties 内部
41
+ let paramsSection = paramsBody;
42
+ const propsIdx = paramsBody.indexOf('"properties"');
43
+ if (propsIdx !== -1) {
44
+ const afterProps = paramsBody.slice(propsIdx + '"properties"'.length);
45
+ const propsBrace = afterProps.indexOf('{');
46
+ if (propsBrace !== -1) {
47
+ paramsSection = afterProps.slice(propsBrace + 1);
48
+ }
49
+ }
50
+ const paramRegex = /"(\w+)"\s*:\s*\{[^}]*"type"/g;
51
+ const paramNames = [];
52
+ let match;
53
+ while ((match = paramRegex.exec(paramsSection)) !== null) {
54
+ paramNames.push(match[1]);
55
+ }
56
+ if (paramNames.length === 0)
57
+ throw new Error('no params found');
58
+ const params = {};
59
+ for (const name of paramNames) {
60
+ const descMatch = paramsSection.match(new RegExp(`"${name}"[^}]*"description"\\s*:\\s*"([^"]*)"`));
61
+ const reqMatch = paramsSection.match(new RegExp(`"${name}"[^}]*"required"\\s*:\\s*(true|false)`));
62
+ params[name] = {
63
+ type: 'string',
64
+ description: descMatch ? descMatch[1] : name,
65
+ required: reqMatch ? reqMatch[1] === 'true' : false,
66
+ };
67
+ }
68
+ return params;
69
+ }
70
+ catch {
71
+ return {
72
+ arguments: {
73
+ type: 'string',
74
+ description: skill.meta.argumentHint
75
+ ? `参数: ${skill.meta.argumentHint}`
76
+ : '传递给 skill 的 JSON 参数(可选)',
77
+ required: false,
78
+ },
79
+ };
80
+ }
81
+ }
82
+ /**
83
+ * 执行 Python skill 脚本,返回真实结果
84
+ *
85
+ * 通过写入临时 .py 文件再执行,避免 shell 转义问题。
86
+ */
87
+ async function executeSkillScript(scriptPath, skill, args) {
88
+ const funcName = skill.meta.name.replace(/-/g, '_');
89
+ const kwargs = [];
90
+ for (const [key, value] of Object.entries(args)) {
91
+ if (key === 'arguments') {
92
+ // 兼容旧的 arguments 字符串模式
93
+ try {
94
+ const parsed = JSON.parse(value);
95
+ for (const [k, v] of Object.entries(parsed)) {
96
+ kwargs.push(`${k}=${JSON.stringify(v)}`);
97
+ }
98
+ }
99
+ catch {
100
+ if (value)
101
+ kwargs.push(`query=${JSON.stringify(value)}`);
102
+ }
103
+ }
104
+ else {
105
+ // 跳过不属于函数签名的参数名(如 LLM 传了 "parameters"),映射为 query
106
+ const paramKey = key === 'parameters' ? 'query' : key;
107
+ kwargs.push(`${paramKey}=${JSON.stringify(value)}`);
108
+ }
109
+ }
110
+ // 写入临时 Python 文件,避免 shell -c 的转义问题
111
+ const tmpFile = path.join(skill.dirPath, `_tmp_run_${Date.now()}.py`);
112
+ const pyCode = [
113
+ 'import sys, json',
114
+ `sys.path.insert(0, ${JSON.stringify(path.dirname(scriptPath))})`,
115
+ `from skill import ${funcName}`,
116
+ `result = ${funcName}(${kwargs.join(', ')})`,
117
+ 'print(json.dumps(result, ensure_ascii=False, indent=2))',
118
+ ].join('\n');
119
+ try {
120
+ fs.writeFileSync(tmpFile, pyCode, 'utf-8');
121
+ const output = await new Promise((resolve, reject) => {
122
+ exec(`python3 ${JSON.stringify(tmpFile)}`, {
123
+ encoding: 'utf-8',
124
+ timeout: 30000,
125
+ maxBuffer: 1024 * 1024,
126
+ env: { ...process.env },
127
+ cwd: skill.dirPath,
128
+ }, (error, stdout, stderr) => {
129
+ if (error) {
130
+ const parts = [];
131
+ if (stderr)
132
+ parts.push(String(stderr).trim());
133
+ if (stdout)
134
+ parts.push(String(stdout).trim());
135
+ if (parts.length === 0)
136
+ parts.push(error.message);
137
+ reject(new Error(parts.join('\n')));
138
+ return;
139
+ }
140
+ resolve(String(stdout).trim());
141
+ });
142
+ });
143
+ return output || '(skill 执行完成,无输出)';
144
+ }
145
+ catch (e) {
146
+ return `[Skill ${skill.meta.name} 执行失败]\n${e.message}`;
147
+ }
148
+ finally {
149
+ // 清理临时文件
150
+ try {
151
+ fs.unlinkSync(tmpFile);
152
+ }
153
+ catch { /* ignore */ }
154
+ }
155
+ }
156
+ /**
157
+ * 回退模式:返回 skill 指令文本(无 Python 脚本时)
158
+ */
159
+ async function executeSkillPrompt(skill, args) {
160
+ let instruction = skill.instruction;
161
+ const rawArgs = args.arguments || '';
162
+ const argParts = rawArgs.split(/\s+/).filter(Boolean);
163
+ instruction = instruction.replace(/\$ARGUMENTS/g, rawArgs);
164
+ for (let i = 0; i < argParts.length; i++) {
165
+ instruction = instruction.replace(new RegExp(`\\$ARGUMENTS\\[${i}\\]`, 'g'), argParts[i]);
166
+ instruction = instruction.replace(new RegExp(`\\${i}\\b`, 'g'), argParts[i]);
167
+ }
168
+ if (rawArgs && !skill.instruction.includes('$ARGUMENTS') && !skill.instruction.match(/\$\d/)) {
169
+ instruction += `\n\n参数: ${rawArgs}`;
170
+ }
171
+ return `[Skill: ${skill.meta.name}]\n\n${instruction}`;
172
+ }
173
+ /**
174
+ * 将 SkillDefinition 转换为 Tool
175
+ */
176
+ function skillToTool(skill) {
177
+ const toolName = `skill_${skill.meta.name}`;
178
+ const scriptPath = path.join(skill.dirPath, 'skill.py');
179
+ const hasScript = fs.existsSync(scriptPath);
180
+ const parameters = hasScript
181
+ ? buildParamsFromScript(scriptPath, skill)
182
+ : {
183
+ arguments: {
184
+ type: 'string',
185
+ description: skill.meta.argumentHint
186
+ ? `参数: ${skill.meta.argumentHint}`
187
+ : '传递给 skill 的参数(可选)',
188
+ required: false,
189
+ },
190
+ };
191
+ return {
192
+ name: toolName,
193
+ description: `[Skill] ${skill.meta.description || skill.meta.name}`,
194
+ parameters,
195
+ execute: hasScript
196
+ ? async (args) => executeSkillScript(scriptPath, skill, args)
197
+ : async (args) => executeSkillPrompt(skill, args),
198
+ };
199
+ }
200
+ // ===== 公开接口 =====
201
+ /** 加载所有外部 skills(带缓存) */
202
+ export function loadExternalSkills() {
203
+ if (_skillCache)
204
+ return _skillCache;
205
+ _skillCache = scanExternalSkills();
206
+ console.log(`[skills] 已加载 ${_skillCache.length} 个外部 skill (${getExternalSkillsDir()})`);
207
+ return _skillCache;
208
+ }
209
+ /** 获取合并后的所有工具:内置 tools + 外部 skills */
210
+ export function getMergedTools() {
211
+ if (_mergedTools)
212
+ return _mergedTools;
213
+ const skills = loadExternalSkills();
214
+ const skillTools = skills
215
+ .filter((s) => s.meta.userInvocable !== false)
216
+ .map(skillToTool);
217
+ _mergedTools = [...builtinTools, ...skillTools];
218
+ return _mergedTools;
219
+ }
220
+ /** 从合并工具列表中按名称查找 */
221
+ export function findMergedTool(name) {
222
+ return getMergedTools().find((t) => t.name === name);
223
+ }
224
+ /** 获取已加载的 skill 列表(用于斜杠命令等) */
225
+ export function listSkills() {
226
+ return loadExternalSkills();
227
+ }
228
+ /** 清除缓存,强制下次重新扫描 */
229
+ export function reloadSkills() {
230
+ _skillCache = null;
231
+ _mergedTools = null;
232
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * 外部 Skill 加载器
3
+ *
4
+ * 扫描 ~/.jarvis/skills/ 目录,解析每个子目录中的 SKILL.md,
5
+ * 提取 frontmatter 元数据和正文指令。
6
+ *
7
+ * Skill 目录结构:
8
+ * ~/.jarvis/skills/<skill-name>/SKILL.md
9
+ * ~/.jarvis/skills/<skill-name>/reference.md (可选)
10
+ * ~/.jarvis/skills/<skill-name>/scripts/ (可选)
11
+ */
12
+ export interface SkillMeta {
13
+ /** skill 名称,省略时用目录名;只允许小写字母、数字、连字符 */
14
+ name: string;
15
+ /** 描述:说明 skill 做什么、什么时候用 */
16
+ description: string;
17
+ /** 参数提示,例如 [issue-number] */
18
+ argumentHint?: string;
19
+ /** 禁止模型自动触发,仅用户手动 /name 调用 */
20
+ disableModelInvocation?: boolean;
21
+ /** 从 / 菜单隐藏,用户不能直接调 */
22
+ userInvocable?: boolean;
23
+ /** 允许使用的工具列表 */
24
+ allowedTools?: string;
25
+ /** 推理强度 */
26
+ effort?: 'low' | 'medium' | 'high' | 'max';
27
+ /** 其他自定义字段 */
28
+ [key: string]: unknown;
29
+ }
30
+ export interface SkillDefinition {
31
+ /** frontmatter 元数据 */
32
+ meta: SkillMeta;
33
+ /** markdown 正文,作为 skill 指令 */
34
+ instruction: string;
35
+ /** skill 目录路径 */
36
+ dirPath: string;
37
+ /** SKILL.md 文件路径 */
38
+ filePath: string;
39
+ }
40
+ /** 外部 skills 根目录:~/.jarvis/skills/ */
41
+ export declare function getExternalSkillsDir(): string;
42
+ /** 扫描 ~/.jarvis/skills/ 下所有子目录,加载 SKILL.md */
43
+ export declare function scanExternalSkills(): SkillDefinition[];
44
+ /** 按名称获取单个 skill */
45
+ export declare function getSkill(name: string, skills: SkillDefinition[]): SkillDefinition | undefined;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * 外部 Skill 加载器
3
+ *
4
+ * 扫描 ~/.jarvis/skills/ 目录,解析每个子目录中的 SKILL.md,
5
+ * 提取 frontmatter 元数据和正文指令。
6
+ *
7
+ * Skill 目录结构:
8
+ * ~/.jarvis/skills/<skill-name>/SKILL.md
9
+ * ~/.jarvis/skills/<skill-name>/reference.md (可选)
10
+ * ~/.jarvis/skills/<skill-name>/scripts/ (可选)
11
+ */
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import os from 'os';
15
+ // ===== 常量 =====
16
+ const JARVIS_DIR = '.jarvis';
17
+ const SKILLS_DIR = 'skills';
18
+ const SKILL_FILENAME = 'SKILL.md';
19
+ /** 外部 skills 根目录:~/.jarvis/skills/ */
20
+ export function getExternalSkillsDir() {
21
+ return path.join(os.homedir(), JARVIS_DIR, SKILLS_DIR);
22
+ }
23
+ // ===== frontmatter 解析 =====
24
+ function parseFrontMatter(raw) {
25
+ const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
26
+ if (!match) {
27
+ return { meta: {}, body: raw };
28
+ }
29
+ const meta = {};
30
+ for (const line of match[1].split('\n')) {
31
+ const idx = line.indexOf(':');
32
+ if (idx === -1)
33
+ continue;
34
+ const key = line.slice(0, idx).trim();
35
+ const val = line.slice(idx + 1).trim();
36
+ meta[key] = val;
37
+ }
38
+ return { meta, body: match[2].trim() };
39
+ }
40
+ // ===== 加载单个 Skill =====
41
+ function loadSkillDir(dirPath, dirName) {
42
+ const filePath = path.join(dirPath, SKILL_FILENAME);
43
+ if (!fs.existsSync(filePath)) {
44
+ return null;
45
+ }
46
+ try {
47
+ const raw = fs.readFileSync(filePath, 'utf-8');
48
+ const { meta, body } = parseFrontMatter(raw);
49
+ // name 默认用目录名
50
+ const name = meta.name || dirName;
51
+ // 校验 name 格式:只允许小写字母、数字、连字符
52
+ if (!/^[a-z0-9-]+$/.test(name)) {
53
+ console.warn(`[skills] 跳过非法 name "${name}"(仅允许小写字母、数字、连字符): ${filePath}`);
54
+ return null;
55
+ }
56
+ if (name.length > 64) {
57
+ console.warn(`[skills] 跳过过长 name "${name}"(最长 64 字符): ${filePath}`);
58
+ return null;
59
+ }
60
+ const skillMeta = {
61
+ name,
62
+ description: meta.description || '',
63
+ argumentHint: meta['argument-hint'] || undefined,
64
+ disableModelInvocation: meta['disable-model-invocation'] === 'true',
65
+ userInvocable: meta['user-invocable'] !== 'false', // 默认 true
66
+ allowedTools: meta['allowed-tools'] || undefined,
67
+ effort: (['low', 'medium', 'high', 'max'].includes(meta.effort) ? meta.effort : undefined),
68
+ };
69
+ return {
70
+ meta: skillMeta,
71
+ instruction: body,
72
+ dirPath,
73
+ filePath,
74
+ };
75
+ }
76
+ catch (err) {
77
+ console.error(`[skills] 加载失败: ${filePath}`, err instanceof Error ? err.message : err);
78
+ return null;
79
+ }
80
+ }
81
+ // ===== 扫描所有外部 Skills =====
82
+ /** 扫描 ~/.jarvis/skills/ 下所有子目录,加载 SKILL.md */
83
+ export function scanExternalSkills() {
84
+ const skillsDir = getExternalSkillsDir();
85
+ if (!fs.existsSync(skillsDir)) {
86
+ return [];
87
+ }
88
+ const results = [];
89
+ try {
90
+ const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
91
+ for (const entry of entries) {
92
+ if (!entry.isDirectory())
93
+ continue;
94
+ const skill = loadSkillDir(path.join(skillsDir, entry.name), entry.name);
95
+ if (skill) {
96
+ results.push(skill);
97
+ }
98
+ }
99
+ }
100
+ catch (err) {
101
+ console.error('[skills] 扫描外部 skills 目录失败:', err instanceof Error ? err.message : err);
102
+ }
103
+ return results;
104
+ }
105
+ /** 按名称获取单个 skill */
106
+ export function getSkill(name, skills) {
107
+ return skills.find((s) => s.meta.name === name);
108
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * create_skill 工具
3
+ *
4
+ * 根据用户需求,调用 LLM 基于 SKILL_INSTRUCTIONS.md 规范自动生成 skill,
5
+ * 并写入 ~/.jarvis/skills/ 目录。
6
+ */
7
+ import { Tool } from '../types/index.js';
8
+ export declare const createSkill: Tool;
@@ -0,0 +1,255 @@
1
+ /**
2
+ * create_skill 工具
3
+ *
4
+ * 根据用户需求,调用 LLM 基于 SKILL_INSTRUCTIONS.md 规范自动生成 skill,
5
+ * 并写入 ~/.jarvis/skills/ 目录。
6
+ */
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import os from 'os';
10
+ import { getExternalSkillsDir } from '../skills/loader.js';
11
+ import { reloadSkills } from '../skills/index.js';
12
+ import { LLMServiceImpl, getDefaultConfig } from '../services/api/llm.js';
13
+ // SKILL_INSTRUCTIONS.md 查找路径:项目根目录 > 用户主目录
14
+ function loadSkillInstructions() {
15
+ const candidates = [
16
+ path.join(process.cwd(), 'SKILL_INSTRUCTIONS.md'),
17
+ path.join(os.homedir(), '.jarvis', 'SKILL_INSTRUCTIONS.md'),
18
+ ];
19
+ for (const p of candidates) {
20
+ if (fs.existsSync(p)) {
21
+ return fs.readFileSync(p, 'utf-8');
22
+ }
23
+ }
24
+ return '';
25
+ }
26
+ /**
27
+ * 加载已有 skill 作为 LLM 参考示例
28
+ * 从 ~/.jarvis/skills/ 和项目 skills/ 目录中读取
29
+ */
30
+ function loadExampleSkills() {
31
+ const examples = [];
32
+ // 扫描项目内 skills/ 目录和 ~/.jarvis/skills/ 目录
33
+ const dirs = [
34
+ path.join(process.cwd(), 'skills'),
35
+ getExternalSkillsDir(),
36
+ ];
37
+ for (const dir of dirs) {
38
+ if (!fs.existsSync(dir))
39
+ continue;
40
+ try {
41
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
42
+ for (const entry of entries) {
43
+ if (!entry.isDirectory())
44
+ continue;
45
+ const skillMdPath = path.join(dir, entry.name, 'SKILL.md');
46
+ const skillPyPath = path.join(dir, entry.name, 'skill.py');
47
+ if (!fs.existsSync(skillMdPath))
48
+ continue;
49
+ const mdContent = fs.readFileSync(skillMdPath, 'utf-8').slice(0, 800);
50
+ let example = `--- 示例 skill: ${entry.name} ---\n[SKILL.md]\n${mdContent}`;
51
+ if (fs.existsSync(skillPyPath)) {
52
+ const pyContent = fs.readFileSync(skillPyPath, 'utf-8').slice(0, 1200);
53
+ example += `\n[skill.py]\n${pyContent}`;
54
+ }
55
+ examples.push(example);
56
+ if (examples.length >= 2)
57
+ break; // 最多取 2 个示例
58
+ }
59
+ }
60
+ catch { /* ignore */ }
61
+ if (examples.length >= 2)
62
+ break;
63
+ }
64
+ if (examples.length === 0)
65
+ return '';
66
+ return '=== 已有 skill 参考示例(请参考其格式和风格) ===\n' + examples.join('\n\n');
67
+ }
68
+ /**
69
+ * 确保 SKILL.md 的 frontmatter 中包含 description 字段
70
+ * 如果缺失则自动补入
71
+ */
72
+ function ensureFrontmatterDescription(skillMd, description) {
73
+ const fmMatch = skillMd.match(/^---\r?\n([\s\S]*?)\r?\n---/);
74
+ if (!fmMatch) {
75
+ // 没有 frontmatter,整个补上
76
+ const fm = `---\ndescription: ${description}\n---\n\n`;
77
+ return fm + skillMd;
78
+ }
79
+ const fmBody = fmMatch[1];
80
+ // 检查是否已有 description 行(允许值为空的情况也算缺失)
81
+ const descLine = fmBody.split('\n').find((l) => /^description\s*:/.test(l));
82
+ if (descLine) {
83
+ const val = descLine.replace(/^description\s*:\s*/, '').trim();
84
+ if (val && val !== '""' && val !== "''") {
85
+ // 已有有效 description,不修改
86
+ return skillMd;
87
+ }
88
+ // description 值为空,替换
89
+ return skillMd.replace(descLine, `description: ${description}`);
90
+ }
91
+ // frontmatter 中没有 description 行,在 name 行后插入
92
+ const nameLine = fmBody.split('\n').find((l) => /^name\s*:/.test(l));
93
+ if (nameLine) {
94
+ return skillMd.replace(nameLine, `${nameLine}\ndescription: ${description}`);
95
+ }
96
+ // 兜底:在 frontmatter 末尾插入
97
+ return skillMd.replace(/\n---/, `\ndescription: ${description}\n---`);
98
+ }
99
+ /**
100
+ * 调用 LLM 生成 skill 内容
101
+ *
102
+ * 返回 JSON: { name, description, argument_hint?, skill_md, skill_py? }
103
+ */
104
+ async function callLLMForSkill(requirement) {
105
+ const instructions = loadSkillInstructions();
106
+ // 加载已有 skill 作为参考示例
107
+ const exampleSkills = loadExampleSkills();
108
+ const systemPrompt = [
109
+ '你是一个 Skill 生成专家。请严格按照以下规范为用户生成 Skill。',
110
+ '',
111
+ '=== Skill 编写规范 ===',
112
+ instructions || '(规范文件未找到,请按通用 SKILL.md 格式生成)',
113
+ '',
114
+ '=== 语言要求 ===',
115
+ '- 所有 description、注释、文档说明必须使用中文',
116
+ '- name 字段使用英文小写+连字符',
117
+ '- 代码中的变量名、函数名使用英文',
118
+ '',
119
+ '=== 输出要求 ===',
120
+ '你必须返回一个合法的 JSON 对象,不要包含任何其他文字、解释或 markdown 代码块标记。',
121
+ 'JSON 结构如下:',
122
+ '{',
123
+ ' "name": "skill 名称,仅小写字母、数字、连字符",',
124
+ ' "description": "中文简短描述,说明功能和触发场景",',
125
+ ' "argument_hint": "参数提示,可选",',
126
+ ' "skill_md": "完整的 SKILL.md 文件内容(包含 frontmatter 和正文)",',
127
+ ' "skill_py": "如果 skill 需要可执行逻辑则必须提供完整 Python 代码"',
128
+ '}',
129
+ '',
130
+ '=== 关键规则 ===',
131
+ '1. skill_md 的 frontmatter 必须包含 name 和 description,description 必须是中文,不能为空',
132
+ '2. skill_md 的 frontmatter 中如果 skill 接受参数,必须包含 argument-hint 字段(如 [message] [target])',
133
+ '3. 如果用户需求涉及任何可执行逻辑(API 调用、网络请求、数据处理、文件操作等),必须生成 skill_py',
134
+ '4. skill_py 中的 TOOL_METADATA["parameters"] 必须是扁平字典结构,每个参数直接作为 key,格式如下:',
135
+ ' "parameters": {',
136
+ ' "query": {',
137
+ ' "type": "string",',
138
+ ' "description": "中文参数说明",',
139
+ ' "required": True',
140
+ ' },',
141
+ ' "max_results": {',
142
+ ' "type": "integer",',
143
+ ' "description": "最大返回结果数",',
144
+ ' "required": False',
145
+ ' }',
146
+ ' }',
147
+ ' 注意: required 使用 Python 的 True/False,不要用 JavaScript 的 true/false',
148
+ ' 注意: 不要嵌套 "properties" 或 "type": "object",直接把参数名作为 key',
149
+ '5. skill_py 中的函数名必须与 name 一致(连字符替换为下划线)',
150
+ '6. skill_py 中的 TOOL_METADATA["description"] 也必须是中文',
151
+ '7. JSON 输出中的字符串值如果包含换行,使用 \\n 转义',
152
+ '8. 有副作用的操作(发消息、部署、写入外部系统)必须设置 disable-model-invocation: true',
153
+ '9. JSON 输出中 argument_hint 字段必须与 skill_md frontmatter 中的 argument-hint 一致',
154
+ '',
155
+ exampleSkills,
156
+ ].join('\n');
157
+ // 收集 LLM 流式输出
158
+ let result = '';
159
+ const service = new LLMServiceImpl({
160
+ ...getDefaultConfig(),
161
+ });
162
+ await new Promise((resolve, reject) => {
163
+ const callbacks = {
164
+ onText: (text) => { result += text; },
165
+ onToolUse: () => { },
166
+ onComplete: () => resolve(),
167
+ onError: (err) => reject(err),
168
+ };
169
+ const messagesWithContext = [
170
+ { role: 'user', content: `${systemPrompt}\n\n---\n\n用户需求: ${requirement}` },
171
+ ];
172
+ service.streamMessage(messagesWithContext, [], callbacks).catch(reject);
173
+ });
174
+ // 解析 JSON 结果
175
+ // LLM 可能返回 ```json ... ``` 包裹的内容,需要提取
176
+ let jsonStr = result.trim();
177
+ const jsonBlockMatch = jsonStr.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
178
+ if (jsonBlockMatch) {
179
+ jsonStr = jsonBlockMatch[1].trim();
180
+ }
181
+ try {
182
+ const parsed = JSON.parse(jsonStr);
183
+ if (!parsed.name || !parsed.skill_md) {
184
+ throw new Error('LLM 返回的 JSON 缺少必要字段 (name, skill_md)');
185
+ }
186
+ if (!parsed.description) {
187
+ throw new Error('LLM 返回的 JSON 缺少 description 字段');
188
+ }
189
+ // 确保 skill_md 的 frontmatter 中包含 description
190
+ parsed.skill_md = ensureFrontmatterDescription(parsed.skill_md, parsed.description);
191
+ return parsed;
192
+ }
193
+ catch (e) {
194
+ if (e.message.startsWith('LLM 返回'))
195
+ throw e;
196
+ throw new Error(`解析 LLM 生成结果失败: ${e.message}\n\n原始输出:\n${result.slice(0, 500)}`);
197
+ }
198
+ }
199
+ export const createSkill = {
200
+ name: 'create_skill',
201
+ description: '根据用户需求,调用大模型基于 SKILL_INSTRUCTIONS.md 规范自动生成 Skill 并写入 ~/.jarvis/skills/',
202
+ parameters: {
203
+ requirement: {
204
+ type: 'string',
205
+ description: '用户对 skill 的需求描述,例如"一个可以查询天气的工具"',
206
+ required: true,
207
+ },
208
+ },
209
+ execute: async (args) => {
210
+ const requirement = (args.requirement || '').trim();
211
+ if (!requirement) {
212
+ throw new Error('缺少必填参数: requirement(skill 需求描述)');
213
+ }
214
+ // 1. 调用 LLM 生成 skill 内容
215
+ const generated = await callLLMForSkill(requirement);
216
+ // 2. 校验 name
217
+ const name = generated.name;
218
+ if (!/^[a-z0-9-]+$/.test(name)) {
219
+ throw new Error(`LLM 生成的 skill 名称 "${name}" 不合法,仅允许小写字母、数字、连字符`);
220
+ }
221
+ if (name.length > 64) {
222
+ throw new Error('LLM 生成的 skill 名称过长,最多 64 个字符');
223
+ }
224
+ const skillsDir = getExternalSkillsDir();
225
+ const skillDir = path.join(skillsDir, name);
226
+ // 3. 检查是否已存在
227
+ if (fs.existsSync(skillDir)) {
228
+ throw new Error(`Skill "${name}" 已存在: ${skillDir}`);
229
+ }
230
+ // 4. 创建目录并写入文件
231
+ fs.mkdirSync(skillDir, { recursive: true });
232
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), generated.skill_md, 'utf-8');
233
+ if (generated.skill_py) {
234
+ fs.writeFileSync(path.join(skillDir, 'skill.py'), generated.skill_py, 'utf-8');
235
+ }
236
+ // 5. 刷新缓存
237
+ reloadSkills();
238
+ // 6. 返回结果
239
+ const parts = [
240
+ `Skill "${name}" 创建成功`,
241
+ ` 描述: ${generated.description}`,
242
+ ` 目录: ${skillDir}`,
243
+ ` SKILL.md: 已生成`,
244
+ ];
245
+ if (generated.skill_py) {
246
+ parts.push(' skill.py: 已生成');
247
+ }
248
+ if (generated.argument_hint) {
249
+ parts.push(` 参数提示: ${generated.argument_hint}`);
250
+ }
251
+ parts.push('', '已刷新 skill 缓存,新 skill 立即可用。');
252
+ parts.push('可通过 /skills 查看,或直接使用 /' + name + ' 调用。');
253
+ return parts.join('\n');
254
+ },
255
+ };
@@ -0,0 +1,16 @@
1
+ import { Tool } from '../types/index.js';
2
+ import { readFile } from './readFile.js';
3
+ import { writeFile } from './writeFile.js';
4
+ import { runCommand } from './runCommand.js';
5
+ import { listDirectory } from './listDirectory.js';
6
+ import { searchFiles } from './searchFiles.js';
7
+ import { createSkill } from './createSkill.js';
8
+ export { readFile, writeFile, runCommand, listDirectory, searchFiles, createSkill };
9
+ /** 所有内置工具 */
10
+ export declare const allTools: Tool[];
11
+ /** 按名称查找内置工具 */
12
+ export declare function findTool(name: string): Tool | undefined;
13
+ /** 获取所有工具(内置 + 外部 skills),供 QueryEngine 使用 */
14
+ export declare function getAllTools(): Tool[];
15
+ /** 按名称查找工具(内置 + 外部 skills) */
16
+ export declare function findToolMerged(name: string): Tool | undefined;