@zhin.js/agent 0.0.17 → 0.0.19

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 (61) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +14 -8
  3. package/lib/builtin-tools.d.ts +5 -137
  4. package/lib/builtin-tools.d.ts.map +1 -1
  5. package/lib/builtin-tools.js +321 -732
  6. package/lib/builtin-tools.js.map +1 -1
  7. package/lib/discover-agents.d.ts +28 -0
  8. package/lib/discover-agents.d.ts.map +1 -0
  9. package/lib/discover-agents.js +116 -0
  10. package/lib/discover-agents.js.map +1 -0
  11. package/lib/discover-skills.d.ts +49 -0
  12. package/lib/discover-skills.d.ts.map +1 -0
  13. package/lib/discover-skills.js +297 -0
  14. package/lib/discover-skills.js.map +1 -0
  15. package/lib/discover-tools.d.ts +56 -0
  16. package/lib/discover-tools.d.ts.map +1 -0
  17. package/lib/discover-tools.js +263 -0
  18. package/lib/discover-tools.js.map +1 -0
  19. package/lib/discovery-utils.d.ts +27 -0
  20. package/lib/discovery-utils.d.ts.map +1 -0
  21. package/lib/discovery-utils.js +96 -0
  22. package/lib/discovery-utils.js.map +1 -0
  23. package/lib/file-policy.d.ts +41 -4
  24. package/lib/file-policy.d.ts.map +1 -1
  25. package/lib/file-policy.js +126 -4
  26. package/lib/file-policy.js.map +1 -1
  27. package/lib/index.d.ts +1 -1
  28. package/lib/index.d.ts.map +1 -1
  29. package/lib/index.js +1 -1
  30. package/lib/index.js.map +1 -1
  31. package/lib/init/create-zhin-agent.d.ts.map +1 -1
  32. package/lib/init/create-zhin-agent.js +3 -1
  33. package/lib/init/create-zhin-agent.js.map +1 -1
  34. package/lib/init/register-builtin-tools.d.ts.map +1 -1
  35. package/lib/init/register-builtin-tools.js +51 -54
  36. package/lib/init/register-builtin-tools.js.map +1 -1
  37. package/lib/zhin-agent/config.js +1 -1
  38. package/lib/zhin-agent/config.js.map +1 -1
  39. package/lib/zhin-agent/exec-policy.d.ts +48 -2
  40. package/lib/zhin-agent/exec-policy.d.ts.map +1 -1
  41. package/lib/zhin-agent/exec-policy.js +184 -23
  42. package/lib/zhin-agent/exec-policy.js.map +1 -1
  43. package/lib/zhin-agent/prompt.d.ts +14 -0
  44. package/lib/zhin-agent/prompt.d.ts.map +1 -1
  45. package/lib/zhin-agent/prompt.js +192 -45
  46. package/lib/zhin-agent/prompt.js.map +1 -1
  47. package/package.json +3 -3
  48. package/src/builtin-tools.ts +333 -835
  49. package/src/discover-agents.ts +138 -0
  50. package/src/discover-skills.ts +325 -0
  51. package/src/discover-tools.ts +302 -0
  52. package/src/discovery-utils.ts +96 -0
  53. package/src/file-policy.ts +152 -4
  54. package/src/index.ts +5 -1
  55. package/src/init/create-zhin-agent.ts +3 -1
  56. package/src/init/register-builtin-tools.ts +51 -62
  57. package/src/zhin-agent/config.ts +1 -1
  58. package/src/zhin-agent/exec-policy.ts +229 -24
  59. package/src/zhin-agent/prompt.ts +209 -47
  60. package/tests/exec-policy.test.ts +355 -0
  61. package/tests/file-policy.test.ts +189 -1
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Agent 预设发现(*.agent.md 文件扫描)
3
+ *
4
+ * 加载顺序与 skills 一致:Workspace > ~/.zhin > data > 插件包
5
+ * 同名先发现者优先
6
+ */
7
+
8
+ import * as fs from 'fs';
9
+ import * as os from 'os';
10
+ import * as path from 'path';
11
+ import { Logger, type Plugin } from '@zhin.js/core';
12
+ import { getDataDir } from './discovery-utils.js';
13
+
14
+ const logger = new Logger(null, 'builtin-tools');
15
+
16
+ // ============================================================================
17
+ // 类型
18
+ // ============================================================================
19
+
20
+ export interface AgentMeta {
21
+ name: string;
22
+ description: string;
23
+ keywords?: string[];
24
+ tags?: string[];
25
+ /** frontmatter 中声明的工具名列表 */
26
+ toolNames?: string[];
27
+ /** *.agent.md 文件的绝对路径 */
28
+ filePath: string;
29
+ /** 首选模型名 */
30
+ model?: string;
31
+ /** 首选 Provider 名 */
32
+ provider?: string;
33
+ /** 最大工具调用迭代次数 */
34
+ maxIterations?: number;
35
+ }
36
+
37
+ // ============================================================================
38
+ // 发现
39
+ // ============================================================================
40
+
41
+ /**
42
+ * 扫描 agents/ 目录,发现 *.agent.md 文件
43
+ */
44
+ export async function discoverWorkspaceAgents(root?: Plugin | null): Promise<AgentMeta[]> {
45
+ const agents: AgentMeta[] = [];
46
+ const seenNames = new Set<string>();
47
+
48
+ const agentDirs: string[] = [
49
+ path.join(process.cwd(), 'agents'),
50
+ path.join(os.homedir(), '.zhin', 'agents'),
51
+ path.join(getDataDir(), 'agents'),
52
+ ];
53
+ if (root) {
54
+ const addPluginDir = (p: Plugin) => {
55
+ if (!p?.filePath) return;
56
+ const dir = path.dirname(p.filePath);
57
+ const d = path.join(dir, 'agents');
58
+ if (!agentDirs.includes(d)) agentDirs.push(d);
59
+ const dirName = path.basename(dir);
60
+ if (dirName === 'src' || dirName === 'lib') {
61
+ const d2 = path.join(path.dirname(dir), 'agents');
62
+ if (!agentDirs.includes(d2)) agentDirs.push(d2);
63
+ }
64
+ };
65
+ addPluginDir(root);
66
+ for (const child of root.children || []) {
67
+ addPluginDir(child);
68
+ }
69
+ }
70
+
71
+ for (const agentsDir of agentDirs) {
72
+ if (!fs.existsSync(agentsDir)) continue;
73
+
74
+ let entries: fs.Dirent[];
75
+ try {
76
+ entries = await fs.promises.readdir(agentsDir, { withFileTypes: true });
77
+ } catch {
78
+ continue;
79
+ }
80
+
81
+ for (const entry of entries) {
82
+ let agentMdPath: string | undefined;
83
+ if (entry.isFile() && entry.name.endsWith('.agent.md')) {
84
+ agentMdPath = path.join(agentsDir, entry.name);
85
+ } else if (entry.isDirectory()) {
86
+ const nested = path.join(agentsDir, entry.name, `${entry.name}.agent.md`);
87
+ if (fs.existsSync(nested)) agentMdPath = nested;
88
+ }
89
+ if (!agentMdPath) continue;
90
+
91
+ try {
92
+ const content = await fs.promises.readFile(agentMdPath, 'utf-8');
93
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*(?:\n|$)/);
94
+ if (!match) {
95
+ logger.debug(`Agent文件 ${agentMdPath} 没有有效的frontmatter格式`);
96
+ continue;
97
+ }
98
+
99
+ let jsYaml: any;
100
+ try {
101
+ jsYaml = await import('js-yaml');
102
+ if (jsYaml.default) jsYaml = jsYaml.default;
103
+ } catch (e) {
104
+ logger.warn(`Unable to import js-yaml module: ${e}`);
105
+ continue;
106
+ }
107
+
108
+ const metadata = jsYaml.load(match[1]);
109
+ if (!metadata || !metadata.name || !metadata.description) {
110
+ logger.debug(`Agent文件 ${agentMdPath} 缺少必需的 name/description 字段`);
111
+ continue;
112
+ }
113
+
114
+ if (seenNames.has(metadata.name)) {
115
+ logger.debug(`Agent '${metadata.name}' 已由先序目录加载,跳过: ${agentMdPath}`);
116
+ continue;
117
+ }
118
+ seenNames.add(metadata.name);
119
+
120
+ agents.push({
121
+ name: metadata.name,
122
+ description: metadata.description,
123
+ keywords: metadata.keywords || [],
124
+ tags: metadata.tags || [],
125
+ toolNames: Array.isArray(metadata.tools) ? metadata.tools : [],
126
+ filePath: agentMdPath,
127
+ model: metadata.model,
128
+ provider: metadata.provider,
129
+ maxIterations: typeof metadata.maxIterations === 'number' ? metadata.maxIterations : undefined,
130
+ });
131
+ logger.debug(`Agent发现成功: ${metadata.name}`);
132
+ } catch (e) {
133
+ logger.warn(`Failed to parse agent.md in ${agentMdPath}:`, e);
134
+ }
135
+ }
136
+ }
137
+ return agents;
138
+ }
@@ -0,0 +1,325 @@
1
+ /**
2
+ * 技能发现(SKILL.md 文件扫描与加载)
3
+ *
4
+ * 加载顺序:Workspace(cwd/skills)> Local(~/.zhin/skills)> data/skills > 已加载插件包 skills/
5
+ * 同名先发现者优先,支持平台/依赖兼容性过滤
6
+ */
7
+
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ import { exec } from 'child_process';
11
+ import { promisify } from 'util';
12
+ import { Logger, type Plugin } from '@zhin.js/core';
13
+ import { getSkillSearchDirectories, getDataDir } from './discovery-utils.js';
14
+
15
+ const execAsync = promisify(exec);
16
+ const logger = new Logger(null, 'builtin-tools');
17
+
18
+ // ============================================================================
19
+ // 类型
20
+ // ============================================================================
21
+
22
+ export interface SkillMeta {
23
+ name: string;
24
+ description: string;
25
+ keywords?: string[];
26
+ tags?: string[];
27
+ /** SKILL.md frontmatter 中声明的关联工具名列表 */
28
+ toolNames?: string[];
29
+ filePath: string;
30
+ /** 是否常驻注入 system prompt(frontmatter always: true) */
31
+ always?: boolean;
32
+ /** 当前环境是否满足依赖(bins/env) */
33
+ available?: boolean;
34
+ /** 缺失的依赖描述(如 "CLI: ffmpeg", "ENV: API_KEY") */
35
+ requiresMissing?: string[];
36
+ }
37
+
38
+ // ============================================================================
39
+ // 发现
40
+ // ============================================================================
41
+
42
+ /**
43
+ * 扫描技能目录,发现 SKILL.md 技能文件
44
+ * @param root 根插件(可选):用于追加插件包内 `skills/` 扫描,与 `activate_skill` 查找路径一致
45
+ */
46
+ export async function discoverWorkspaceSkills(root?: Plugin | null): Promise<SkillMeta[]> {
47
+ const skills: SkillMeta[] = [];
48
+ const seenNames = new Set<string>();
49
+ const dataDir = getDataDir();
50
+ const skillDirs = getSkillSearchDirectories(root ?? undefined);
51
+
52
+ // 确保 data/skills 目录存在
53
+ const defaultSkillDir = path.join(dataDir, 'skills');
54
+ if (!fs.existsSync(defaultSkillDir)) {
55
+ fs.mkdirSync(defaultSkillDir, { recursive: true });
56
+ logger.debug(`Created skill directory: ${defaultSkillDir}`);
57
+ }
58
+
59
+ for (const skillsDir of skillDirs) {
60
+ if (!fs.existsSync(skillsDir)) continue;
61
+
62
+ let entries: fs.Dirent[];
63
+ try {
64
+ entries = await fs.promises.readdir(skillsDir, { withFileTypes: true });
65
+ } catch {
66
+ continue;
67
+ }
68
+
69
+ for (const entry of entries) {
70
+ if (!entry.isDirectory()) continue;
71
+ const skillMdPath = path.join(skillsDir, entry.name, 'SKILL.md');
72
+ if (!fs.existsSync(skillMdPath)) continue;
73
+
74
+ try {
75
+ const content = await fs.promises.readFile(skillMdPath, 'utf-8');
76
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*(?:\n|$)/);
77
+ if (!match) {
78
+ logger.debug(`Skill文件 ${skillMdPath} 没有有效的frontmatter格式`);
79
+ continue;
80
+ }
81
+
82
+ let jsYaml: any;
83
+ try {
84
+ jsYaml = await import('js-yaml');
85
+ if (jsYaml.default) jsYaml = jsYaml.default;
86
+ } catch (e) {
87
+ logger.warn(`Unable to import js-yaml module: ${e}`);
88
+ continue;
89
+ }
90
+
91
+ const metadata = jsYaml.load(match[1]);
92
+ if (!metadata || !metadata.name || !metadata.description) {
93
+ logger.debug(`Skill文件 ${skillMdPath} 缺少必需的 name/description 字段`);
94
+ continue;
95
+ }
96
+
97
+ // 平台兼容检查
98
+ const compat = metadata.compatibility || {};
99
+ if (compat.os && Array.isArray(compat.os)) {
100
+ const currentOs = process.platform === 'darwin' ? 'darwin' : process.platform === 'win32' ? 'windows' : 'linux';
101
+ if (!compat.os.includes(currentOs)) {
102
+ logger.debug(`Skipping skill '${metadata.name}' (unsupported OS)`);
103
+ continue;
104
+ }
105
+ }
106
+
107
+ // 依赖检查
108
+ const requiresBins: string[] = metadata.requires?.bins || compat.deps || metadata.deps || [];
109
+ const requiresEnv: string[] = metadata.requires?.env || [];
110
+ const binsToCheck = Array.isArray(requiresBins) ? requiresBins : [];
111
+ const envToCheck = Array.isArray(requiresEnv) ? requiresEnv : [];
112
+ const requiresMissing: string[] = [];
113
+ for (const bin of binsToCheck) {
114
+ try {
115
+ await execAsync(`which ${bin} 2>/dev/null`);
116
+ } catch {
117
+ requiresMissing.push(`CLI: ${bin}`);
118
+ }
119
+ }
120
+ for (const envKey of envToCheck) {
121
+ if (!process.env[envKey]) {
122
+ requiresMissing.push(`ENV: ${envKey}`);
123
+ }
124
+ }
125
+ const available = requiresMissing.length === 0;
126
+
127
+ if (seenNames.has(metadata.name)) {
128
+ logger.debug(`Skill '${metadata.name}' 已由先序目录加载,跳过: ${skillMdPath}`);
129
+ continue;
130
+ }
131
+ seenNames.add(metadata.name);
132
+
133
+ skills.push({
134
+ name: metadata.name,
135
+ description: metadata.description,
136
+ keywords: metadata.keywords || [],
137
+ tags: [...(metadata.tags || []), 'workspace-skill'],
138
+ toolNames: Array.isArray(metadata.tools) ? metadata.tools : [],
139
+ filePath: skillMdPath,
140
+ always: Boolean(metadata.always),
141
+ available,
142
+ requiresMissing: requiresMissing.length > 0 ? requiresMissing : undefined,
143
+ });
144
+ logger.debug(`Skill发现成功: ${metadata.name}, tools: ${JSON.stringify(metadata.tools || [])}`);
145
+ } catch (e) {
146
+ logger.warn(`Failed to parse SKILL.md in ${skillMdPath}:`, e);
147
+ }
148
+ }
149
+ }
150
+
151
+ return skills;
152
+ }
153
+
154
+ // ============================================================================
155
+ // 辅助函数
156
+ // ============================================================================
157
+
158
+ /**
159
+ * 获取 frontmatter 中 always: true 的技能名列表(用于常驻注入 system prompt)
160
+ */
161
+ export function getAlwaysSkillNames(skills: SkillMeta[]): string[] {
162
+ return skills.filter(s => s.always && s.available).map(s => s.name);
163
+ }
164
+
165
+ function stripFrontmatter(content: string): string {
166
+ const match = content.match(/^---\s*\n[\s\S]*?\n---\s*(?:\n|$)/);
167
+ if (match) return content.slice(match[0].length).trim();
168
+ return content.trim();
169
+ }
170
+
171
+ /**
172
+ * 加载 always 技能的正文内容并拼接为「Active Skills」段
173
+ */
174
+ export async function loadAlwaysSkillsContent(skills: SkillMeta[]): Promise<string> {
175
+ const always = skills.filter(s => s.always && s.available);
176
+ if (always.length === 0) return '';
177
+ const parts: string[] = [];
178
+ for (const s of always) {
179
+ try {
180
+ const content = await fs.promises.readFile(s.filePath, 'utf-8');
181
+ const body = stripFrontmatter(content);
182
+ parts.push(`### Skill: ${s.name}\n\n${body}`);
183
+ } catch (e) {
184
+ logger.warn(`Failed to load always skill ${s.name}: ${(e as Error).message}`);
185
+ }
186
+ }
187
+ return parts.join('\n\n---\n\n');
188
+ }
189
+
190
+ function escapeXml(s: string): string {
191
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
192
+ }
193
+
194
+ /**
195
+ * 构建技能列表的 XML 摘要,供 model 区分可用/不可用及缺失依赖
196
+ */
197
+ export function buildSkillsSummaryXML(skills: SkillMeta[]): string {
198
+ if (skills.length === 0) return '';
199
+ const lines = ['<skills>'];
200
+ for (const s of skills) {
201
+ const available = s.available !== false;
202
+ lines.push(` <skill available="${available}">`);
203
+ lines.push(` <name>${escapeXml(s.name)}</name>`);
204
+ lines.push(` <description>${escapeXml(s.description)}</description>`);
205
+ lines.push(` <location>${escapeXml(s.filePath)}</location>`);
206
+ if (!available && s.requiresMissing && s.requiresMissing.length > 0) {
207
+ lines.push(` <requires>${escapeXml(s.requiresMissing.join(', '))}</requires>`);
208
+ }
209
+ lines.push(' </skill>');
210
+ }
211
+ lines.push('</skills>');
212
+ return lines.join('\n');
213
+ }
214
+
215
+ // ============================================================================
216
+ // 技能内容解析(activate_skill 使用)
217
+ // ============================================================================
218
+
219
+ /**
220
+ * 检查技能声明的依赖是否在环境中可用;若有缺失返回提示文案,否则返回空字符串
221
+ */
222
+ export async function checkSkillDeps(content: string): Promise<string> {
223
+ const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
224
+ if (!fmMatch) return '';
225
+ let jsYaml: any;
226
+ try {
227
+ jsYaml = await import('js-yaml');
228
+ if (jsYaml.default) jsYaml = jsYaml.default;
229
+ } catch {
230
+ return '';
231
+ }
232
+ const metadata = jsYaml.load(fmMatch[1]);
233
+ if (!metadata) return '';
234
+ const compat = metadata.compatibility || {};
235
+ const deps = compat.deps || metadata.deps;
236
+ if (!deps || !Array.isArray(deps)) return '';
237
+ const missing: string[] = [];
238
+ for (const dep of deps) {
239
+ try {
240
+ await execAsync(`which ${dep} 2>/dev/null`);
241
+ } catch {
242
+ missing.push(dep);
243
+ }
244
+ }
245
+ if (missing.length === 0) return '';
246
+ return `⚠️ 当前环境缺少以下依赖,请先安装后再使用本技能:${missing.join(', ')}`;
247
+ }
248
+
249
+ /**
250
+ * 从 SKILL.md 全文中提取精简的执行指令
251
+ * 只保留 frontmatter(工具列表)和执行规则,去掉示例、测试场景等冗余内容
252
+ */
253
+ export function extractSkillInstructions(name: string, content: string, maxBodyLen: number = 4000): string {
254
+ const lines: string[] = [];
255
+ lines.push(`Skill '${name}' activated. 请立即根据以下指导执行工具调用:`);
256
+ lines.push('');
257
+
258
+ const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
259
+ if (fmMatch) {
260
+ const fmContent = fmMatch[1];
261
+ const toolsMatch = fmContent.match(/tools:\s*\n((?:\s+-\s+.+\n?)+)/);
262
+ if (toolsMatch) {
263
+ lines.push('## 可用工具');
264
+ lines.push(toolsMatch[0].trim());
265
+ lines.push('');
266
+ }
267
+ }
268
+
269
+ const bodyAfterFm = fmMatch && fmMatch.index !== undefined
270
+ ? content.slice(fmMatch.index + fmMatch[0].length).replace(/^\s+/, '')
271
+ : content;
272
+
273
+ // Priority: "## 快速操作" / "## Quick Actions" summary for small models
274
+ const quickActionsMatch = bodyAfterFm.match(/## (?:快速操作|Quick\s*Actions)[\s\S]*?(?=\n## [^\s]|$)/i);
275
+ if (quickActionsMatch && maxBodyLen <= 2000) {
276
+ lines.push(quickActionsMatch[0].trim());
277
+ lines.push('');
278
+ lines.push('## 立即行动');
279
+ lines.push('根据上面的指导,立即调用工具完成用户请求。禁止重复调用 activate_skill,禁止用文本描述代替实际工具调用。');
280
+ return lines.join('\n');
281
+ }
282
+
283
+ const rulesMatch = content.match(/## 执行规则[\s\S]*?(?=\n## [^\s]|$)/);
284
+ const workflowMatch = content.match(/## (?:Workflow|Instructions|使用说明)[\s\S]*?(?=\n## [^\s]|$)/);
285
+
286
+ if (rulesMatch) {
287
+ lines.push(rulesMatch[0].trim());
288
+ lines.push('');
289
+ } else if (workflowMatch) {
290
+ lines.push(workflowMatch[0].trim());
291
+ lines.push('');
292
+ } else if (bodyAfterFm.trim()) {
293
+ const firstH2 = bodyAfterFm.match(/\n## [^\s]/);
294
+ const intro = firstH2 ? bodyAfterFm.slice(0, firstH2.index).trim() : bodyAfterFm.trim();
295
+
296
+ const quickStartMatch = bodyAfterFm.match(/## (?:快速开始|Quick\s*Start|Getting\s*Started)[\s\S]*?(?=\n## [^\s]|$)/i);
297
+ const authMatch = bodyAfterFm.match(/## (?:认证|Authentication|Auth)[\s\S]*?(?=\n## [^\s]|$)/i);
298
+
299
+ if (quickStartMatch || (intro.length < 200 && bodyAfterFm.length > intro.length)) {
300
+ lines.push('## 指导');
301
+ lines.push(intro);
302
+ lines.push('');
303
+ const extra: string[] = [];
304
+ if (quickStartMatch) extra.push(quickStartMatch[0].trim());
305
+ if (authMatch) extra.push(authMatch[0].trim());
306
+ if (extra.length > 0) {
307
+ const joined = extra.join('\n\n');
308
+ lines.push(joined.length > maxBodyLen ? joined.slice(0, maxBodyLen) + '\n...(truncated)' : joined);
309
+ } else {
310
+ const rest = bodyAfterFm.slice(intro.length).trim();
311
+ lines.push(rest.length > maxBodyLen ? rest.slice(0, maxBodyLen) + '\n...(truncated)' : rest);
312
+ }
313
+ lines.push('');
314
+ } else if (intro) {
315
+ lines.push('## 指导');
316
+ lines.push(intro);
317
+ lines.push('');
318
+ }
319
+ }
320
+
321
+ lines.push('## 立即行动');
322
+ lines.push('根据上面的指导,立即调用工具完成用户请求。禁止重复调用 activate_skill,禁止用文本描述代替实际工具调用。');
323
+
324
+ return lines.join('\n');
325
+ }