@probelabs/probe 0.6.0-rc200 → 0.6.0-rc202
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 +31 -1
- package/bin/binaries/probe-v0.6.0-rc202-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc202-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc202-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc202-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc202-x86_64-unknown-linux-musl.tar.gz +0 -0
- package/build/agent/ProbeAgent.d.ts +19 -1
- package/build/agent/ProbeAgent.js +310 -14
- package/build/agent/index.js +8615 -394
- package/build/agent/probeTool.js +2 -2
- package/build/agent/schemaUtils.js +37 -18
- package/build/agent/shared/prompts.js +17 -0
- package/build/agent/skills/formatting.js +23 -0
- package/build/agent/skills/parser.js +162 -0
- package/build/agent/skills/registry.js +185 -0
- package/build/agent/skills/tools.js +65 -0
- package/build/agent/tools.js +44 -0
- package/build/delegate.js +27 -7
- package/build/tools/common.js +17 -4
- package/build/tools/system-message.js +4 -4
- package/build/tools/vercel.js +243 -36
- package/cjs/agent/ProbeAgent.cjs +8769 -583
- package/cjs/index.cjs +8794 -608
- package/index.d.ts +16 -0
- package/package.json +2 -1
- package/scripts/postinstall.js +10 -4
- package/src/agent/ProbeAgent.d.ts +19 -1
- package/src/agent/ProbeAgent.js +310 -14
- package/src/agent/index.js +21 -1
- package/src/agent/probeTool.js +2 -2
- package/src/agent/schemaUtils.js +37 -18
- package/src/agent/shared/prompts.js +17 -0
- package/src/agent/skills/formatting.js +23 -0
- package/src/agent/skills/parser.js +162 -0
- package/src/agent/skills/registry.js +185 -0
- package/src/agent/skills/tools.js +65 -0
- package/src/agent/tools.js +44 -0
- package/src/delegate.js +27 -7
- package/src/tools/common.js +17 -4
- package/src/tools/system-message.js +4 -4
- package/src/tools/vercel.js +243 -36
- package/bin/binaries/probe-v0.6.0-rc200-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc200-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc200-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc200-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc200-x86_64-unknown-linux-musl.tar.gz +0 -0
package/build/agent/probeTool.js
CHANGED
|
@@ -59,7 +59,7 @@ export function clearToolExecutionData(sessionId) {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
// Wrap the tools to emit events and handle cancellation
|
|
62
|
-
const wrapToolWithEmitter = (tool, toolName, baseExecute) => {
|
|
62
|
+
export const wrapToolWithEmitter = (tool, toolName, baseExecute) => {
|
|
63
63
|
return {
|
|
64
64
|
...tool, // Spread schema, description etc.
|
|
65
65
|
execute: async (params) => { // The execute function now receives parsed params
|
|
@@ -407,4 +407,4 @@ export const searchFilesTool = {
|
|
|
407
407
|
|
|
408
408
|
// Wrap the additional tools
|
|
409
409
|
export const listFilesToolInstance = wrapToolWithEmitter(listFilesTool, 'listFiles', listFilesTool.execute);
|
|
410
|
-
export const searchFilesToolInstance = wrapToolWithEmitter(searchFilesTool, 'searchFiles', searchFilesTool.execute);
|
|
410
|
+
export const searchFilesToolInstance = wrapToolWithEmitter(searchFilesTool, 'searchFiles', searchFilesTool.execute);
|
|
@@ -1143,6 +1143,28 @@ export function replaceMermaidDiagramsInMarkdown(originalResponse, correctedDiag
|
|
|
1143
1143
|
return modifiedResponse;
|
|
1144
1144
|
}
|
|
1145
1145
|
|
|
1146
|
+
function replaceSingleMermaidDiagramInResponse(response, originalDiagram, newContent) {
|
|
1147
|
+
if (!originalDiagram) {
|
|
1148
|
+
return response;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const attributesStr = originalDiagram.attributes ? ` ${originalDiagram.attributes}` : '';
|
|
1152
|
+
const newCodeBlock = `\`\`\`mermaid${attributesStr}\n${newContent}\n\`\`\``;
|
|
1153
|
+
|
|
1154
|
+
if (originalDiagram.isInJson) {
|
|
1155
|
+
return replaceMermaidDiagramsInJson(response, [
|
|
1156
|
+
{
|
|
1157
|
+
...originalDiagram,
|
|
1158
|
+
content: newContent
|
|
1159
|
+
}
|
|
1160
|
+
]);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
return response.slice(0, originalDiagram.startIndex) +
|
|
1164
|
+
newCodeBlock +
|
|
1165
|
+
response.slice(originalDiagram.endIndex);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1146
1168
|
/**
|
|
1147
1169
|
* Validate a single Mermaid diagram
|
|
1148
1170
|
* @param {string} diagram - Mermaid diagram code
|
|
@@ -1849,12 +1871,11 @@ export async function validateAndFixMermaidResponse(response, options = {}) {
|
|
|
1849
1871
|
if (maidResult.errors.length === 0) {
|
|
1850
1872
|
// Maid fixed it completely
|
|
1851
1873
|
const originalDiagram = diagrams[invalidDiagram.originalIndex];
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
fixedResponse.slice(originalDiagram.endIndex);
|
|
1874
|
+
fixedResponse = replaceSingleMermaidDiagramInResponse(
|
|
1875
|
+
fixedResponse,
|
|
1876
|
+
originalDiagram,
|
|
1877
|
+
maidResult.fixed
|
|
1878
|
+
);
|
|
1858
1879
|
|
|
1859
1880
|
fixingResults.push({
|
|
1860
1881
|
diagramIndex: invalidDiagram.originalIndex,
|
|
@@ -1874,12 +1895,11 @@ export async function validateAndFixMermaidResponse(response, options = {}) {
|
|
|
1874
1895
|
} else if (maidResult.wasFixed) {
|
|
1875
1896
|
// Maid improved it but didn't fix everything - update content for AI fixing
|
|
1876
1897
|
const originalDiagram = diagrams[invalidDiagram.originalIndex];
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
fixedResponse.slice(originalDiagram.endIndex);
|
|
1898
|
+
fixedResponse = replaceSingleMermaidDiagramInResponse(
|
|
1899
|
+
fixedResponse,
|
|
1900
|
+
originalDiagram,
|
|
1901
|
+
maidResult.fixed
|
|
1902
|
+
);
|
|
1883
1903
|
|
|
1884
1904
|
fixingResults.push({
|
|
1885
1905
|
diagramIndex: invalidDiagram.originalIndex,
|
|
@@ -1987,12 +2007,11 @@ export async function validateAndFixMermaidResponse(response, options = {}) {
|
|
|
1987
2007
|
if (fixedContent && fixedContent !== invalidDiagram.content) {
|
|
1988
2008
|
// Replace the diagram in the response
|
|
1989
2009
|
const originalDiagram = updatedDiagrams[invalidDiagram.originalIndex];
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
fixedResponse.slice(originalDiagram.endIndex);
|
|
2010
|
+
fixedResponse = replaceSingleMermaidDiagramInResponse(
|
|
2011
|
+
fixedResponse,
|
|
2012
|
+
originalDiagram,
|
|
2013
|
+
fixedContent
|
|
2014
|
+
);
|
|
1996
2015
|
|
|
1997
2016
|
// Find existing result or create new one
|
|
1998
2017
|
const existingResultIndex = fixingResults.findIndex(r => r.diagramIndex === invalidDiagram.originalIndex);
|
|
@@ -20,6 +20,23 @@ When providing answers:
|
|
|
20
20
|
- Group references by file when multiple locations are from the same file
|
|
21
21
|
- Include brief descriptions of what each reference contains`,
|
|
22
22
|
|
|
23
|
+
'code-searcher': `You are ProbeChat Code Searcher, a specialized AI assistant focused ONLY on locating relevant code. Your sole job is to find and return ALL relevant code locations. Do NOT answer questions or explain anything.
|
|
24
|
+
|
|
25
|
+
When searching:
|
|
26
|
+
- Use only the search tool
|
|
27
|
+
- Run additional searches only if needed to capture all relevant locations
|
|
28
|
+
- Prefer specific, focused queries
|
|
29
|
+
|
|
30
|
+
Output format (MANDATORY):
|
|
31
|
+
- Return ONLY valid JSON with a single top-level key: "targets"
|
|
32
|
+
- "targets" must be an array of strings
|
|
33
|
+
- Each string must be a file target in one of these formats:
|
|
34
|
+
- "path/to/file.ext#SymbolName"
|
|
35
|
+
- "path/to/file.ext:line"
|
|
36
|
+
- "path/to/file.ext:start-end"
|
|
37
|
+
- Prefer #SymbolName when a function/class name is clear; otherwise use line numbers
|
|
38
|
+
- Deduplicate targets and keep them concise`,
|
|
39
|
+
|
|
23
40
|
'architect': `You are ProbeChat Architect, a specialized AI assistant focused on software architecture and design. Your primary function is to help users understand, analyze, and design software systems using the provided code analysis tools.
|
|
24
41
|
|
|
25
42
|
When analyzing code:
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
function escapeXml(value) {
|
|
2
|
+
return value
|
|
3
|
+
.replace(/&/g, '&')
|
|
4
|
+
.replace(/</g, '<')
|
|
5
|
+
.replace(/>/g, '>')
|
|
6
|
+
.replace(/"/g, '"')
|
|
7
|
+
.replace(/'/g, ''');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function formatAvailableSkillsXml(skills) {
|
|
11
|
+
if (!skills || skills.length === 0) return '';
|
|
12
|
+
|
|
13
|
+
const lines = ['<available_skills>'];
|
|
14
|
+
for (const skill of skills) {
|
|
15
|
+
lines.push(' <skill>');
|
|
16
|
+
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
17
|
+
lines.push(` <description>${escapeXml(skill.description || '')}</description>`);
|
|
18
|
+
lines.push(' </skill>');
|
|
19
|
+
}
|
|
20
|
+
lines.push('</available_skills>');
|
|
21
|
+
|
|
22
|
+
return lines.join('\n');
|
|
23
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
|
+
import { dirname } from 'path';
|
|
3
|
+
import YAML from 'yaml';
|
|
4
|
+
|
|
5
|
+
const SKILL_NAME_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
6
|
+
const MAX_SKILL_NAME_LENGTH = 64;
|
|
7
|
+
const MAX_DESCRIPTION_CHARS = 400;
|
|
8
|
+
|
|
9
|
+
function isValidSkillName(name) {
|
|
10
|
+
if (!name || typeof name !== 'string') return false;
|
|
11
|
+
if (name.length > MAX_SKILL_NAME_LENGTH) return false;
|
|
12
|
+
return SKILL_NAME_REGEX.test(name);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getFirstParagraph(text) {
|
|
16
|
+
const lines = text.split(/\r?\n/);
|
|
17
|
+
const paragraphLines = [];
|
|
18
|
+
|
|
19
|
+
for (const line of lines) {
|
|
20
|
+
if (line.trim() === '') {
|
|
21
|
+
if (paragraphLines.length > 0) {
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
paragraphLines.push(line.trim());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return paragraphLines.join(' ').trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function extractFrontmatter(content) {
|
|
34
|
+
const trimmed = content.replace(/^\uFEFF/, '');
|
|
35
|
+
const lines = trimmed.split(/\r?\n/);
|
|
36
|
+
|
|
37
|
+
if (lines.length === 0 || lines[0].trim() !== '---') {
|
|
38
|
+
return { hasFrontmatter: false, frontmatterText: '', body: trimmed };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let endIndex = -1;
|
|
42
|
+
for (let i = 1; i < lines.length; i++) {
|
|
43
|
+
if (lines[i].trim() === '---') {
|
|
44
|
+
endIndex = i;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (endIndex === -1) {
|
|
50
|
+
return { hasFrontmatter: true, invalid: true, frontmatterText: '', body: '' };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const frontmatterText = lines.slice(1, endIndex).join('\n');
|
|
54
|
+
const body = lines.slice(endIndex + 1).join('\n');
|
|
55
|
+
|
|
56
|
+
return { hasFrontmatter: true, frontmatterText, body };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function truncateDescription(text) {
|
|
60
|
+
if (!text) return '';
|
|
61
|
+
const trimmed = text.trim();
|
|
62
|
+
if (trimmed.length <= MAX_DESCRIPTION_CHARS) return trimmed;
|
|
63
|
+
return `${trimmed.slice(0, MAX_DESCRIPTION_CHARS - 3)}...`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeFrontmatter(data) {
|
|
67
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) return {};
|
|
68
|
+
return data;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function deriveSkillName(rawName, directoryName, { debug, skillFilePath }) {
|
|
72
|
+
const candidate = rawName || directoryName;
|
|
73
|
+
if (isValidSkillName(candidate)) return candidate;
|
|
74
|
+
|
|
75
|
+
if (rawName && debug) {
|
|
76
|
+
console.warn(`[skills] Invalid skill name '${rawName}' in ${skillFilePath}; falling back to directory name`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (isValidSkillName(directoryName)) {
|
|
80
|
+
return directoryName;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (debug) {
|
|
84
|
+
console.warn(`[skills] Invalid directory name '${directoryName}' for skill at ${skillFilePath}; skipping`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function deriveDescription(rawDescription, body) {
|
|
91
|
+
let description = rawDescription || '';
|
|
92
|
+
if (!description) {
|
|
93
|
+
description = getFirstParagraph(body);
|
|
94
|
+
}
|
|
95
|
+
return truncateDescription(description);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function stripFrontmatter(content) {
|
|
99
|
+
const { body } = extractFrontmatter(content);
|
|
100
|
+
return body.trim();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function createError(code, message) {
|
|
104
|
+
return { code, message };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function parseSkillFile(skillFilePath, directoryName) {
|
|
108
|
+
let content;
|
|
109
|
+
try {
|
|
110
|
+
content = await readFile(skillFilePath, 'utf8');
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return {
|
|
113
|
+
skill: null,
|
|
114
|
+
error: createError('read_failed', error.message)
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const { hasFrontmatter, frontmatterText, body, invalid } = extractFrontmatter(content);
|
|
119
|
+
if (invalid) {
|
|
120
|
+
return {
|
|
121
|
+
skill: null,
|
|
122
|
+
error: createError('invalid_frontmatter', 'Missing closing frontmatter delimiter')
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let data = {};
|
|
127
|
+
if (hasFrontmatter) {
|
|
128
|
+
try {
|
|
129
|
+
data = YAML.parse(frontmatterText, { schema: 'failsafe' }) || {};
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return {
|
|
132
|
+
skill: null,
|
|
133
|
+
error: createError('invalid_yaml', error.message)
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
data = normalizeFrontmatter(data);
|
|
139
|
+
|
|
140
|
+
const rawName = typeof data.name === 'string' ? data.name.trim() : '';
|
|
141
|
+
const name = deriveSkillName(rawName, directoryName, { debug: false, skillFilePath });
|
|
142
|
+
if (!name) {
|
|
143
|
+
return {
|
|
144
|
+
skill: null,
|
|
145
|
+
error: createError('invalid_name', 'Skill name is invalid')
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const rawDescription = typeof data.description === 'string' ? data.description.trim() : '';
|
|
150
|
+
const description = deriveDescription(rawDescription, body);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
skill: {
|
|
154
|
+
name,
|
|
155
|
+
description,
|
|
156
|
+
skillFilePath,
|
|
157
|
+
directoryName,
|
|
158
|
+
sourceDir: dirname(skillFilePath)
|
|
159
|
+
},
|
|
160
|
+
error: null
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { readdir, readFile, realpath, lstat } from 'fs/promises';
|
|
3
|
+
import { resolve, join, isAbsolute, sep, relative } from 'path';
|
|
4
|
+
import { parseSkillFile, stripFrontmatter } from './parser.js';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_SKILL_DIRS = ['.claude/skills', '.codex/skills', 'skills', '.skills'];
|
|
7
|
+
const SKILL_FILE_NAME = 'SKILL.md';
|
|
8
|
+
|
|
9
|
+
function isPathInside(basePath, targetPath) {
|
|
10
|
+
const base = resolve(basePath);
|
|
11
|
+
const target = resolve(targetPath);
|
|
12
|
+
const rel = relative(base, target);
|
|
13
|
+
if (rel === '') return true;
|
|
14
|
+
if (rel === '..' || rel.startsWith(`..${sep}`)) return false;
|
|
15
|
+
if (isAbsolute(rel)) return false;
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isSafeEntryName(name) {
|
|
20
|
+
if (!name || name === '.' || name === '..') return false;
|
|
21
|
+
if (name.includes('\0')) return false;
|
|
22
|
+
return !name.includes('/') && !name.includes('\\');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class SkillRegistry {
|
|
26
|
+
constructor({ repoRoot, skillDirs = DEFAULT_SKILL_DIRS, debug = false } = {}) {
|
|
27
|
+
this.repoRoot = repoRoot ? resolve(repoRoot) : process.cwd();
|
|
28
|
+
this.repoRootReal = null;
|
|
29
|
+
this.skillDirs = Array.isArray(skillDirs) && skillDirs.length > 0 ? skillDirs : DEFAULT_SKILL_DIRS;
|
|
30
|
+
this.debug = debug;
|
|
31
|
+
this.skills = [];
|
|
32
|
+
this.skillsByName = new Map();
|
|
33
|
+
this.loadErrors = [];
|
|
34
|
+
this.loaded = false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async loadSkills() {
|
|
38
|
+
if (this.loaded) return this.skills;
|
|
39
|
+
|
|
40
|
+
this.loadErrors = [];
|
|
41
|
+
this.repoRootReal = await this._resolveRealPath(this.repoRoot);
|
|
42
|
+
if (!this.repoRootReal) {
|
|
43
|
+
if (this.debug) {
|
|
44
|
+
console.warn(`[skills] Failed to resolve repo root: ${this.repoRoot}`);
|
|
45
|
+
}
|
|
46
|
+
this.loaded = true;
|
|
47
|
+
return this.skills;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const discovered = [];
|
|
51
|
+
for (const skillDir of this.skillDirs) {
|
|
52
|
+
const resolvedDir = await this._resolveSkillDir(skillDir);
|
|
53
|
+
if (!resolvedDir) continue;
|
|
54
|
+
const skillsInDir = await this._scanSkillDir(resolvedDir);
|
|
55
|
+
discovered.push(...skillsInDir);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.skills = discovered;
|
|
59
|
+
this.loaded = true;
|
|
60
|
+
return this.skills;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
getSkills() {
|
|
64
|
+
return this.skills;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getLoadErrors() {
|
|
68
|
+
return this.loadErrors;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getSkill(name) {
|
|
72
|
+
return this.skillsByName.get(name);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async loadSkillInstructions(name) {
|
|
76
|
+
const skill = this.skillsByName.get(name);
|
|
77
|
+
if (!skill) return null;
|
|
78
|
+
|
|
79
|
+
const content = await readFile(skill.skillFilePath, 'utf8');
|
|
80
|
+
return stripFrontmatter(content);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async _resolveRealPath(target) {
|
|
84
|
+
try {
|
|
85
|
+
return await realpath(target);
|
|
86
|
+
} catch (_error) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async _resolveSkillDir(skillDir) {
|
|
92
|
+
const resolved = isAbsolute(skillDir) ? resolve(skillDir) : resolve(this.repoRoot, skillDir);
|
|
93
|
+
const repoRoot = this.repoRootReal || resolve(this.repoRoot);
|
|
94
|
+
const resolvedReal = await this._resolveRealPath(resolved);
|
|
95
|
+
if (!resolvedReal) return null;
|
|
96
|
+
|
|
97
|
+
if (!isPathInside(repoRoot, resolvedReal)) {
|
|
98
|
+
if (this.debug) {
|
|
99
|
+
console.warn(`[skills] Skipping skill dir outside repo: ${resolvedReal}`);
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return resolvedReal;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async _scanSkillDir(dirPath) {
|
|
108
|
+
if (!existsSync(dirPath)) return [];
|
|
109
|
+
|
|
110
|
+
let entries;
|
|
111
|
+
try {
|
|
112
|
+
entries = await readdir(dirPath, { withFileTypes: true });
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (this.debug) {
|
|
115
|
+
console.warn(`[skills] Failed to read skill dir ${dirPath}: ${error.message}`);
|
|
116
|
+
}
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const results = [];
|
|
121
|
+
for (const entry of entries) {
|
|
122
|
+
if (!entry.isDirectory()) continue;
|
|
123
|
+
if (!isSafeEntryName(entry.name)) {
|
|
124
|
+
if (this.debug) {
|
|
125
|
+
console.warn(`[skills] Skipping unsafe skill directory name: ${entry.name}`);
|
|
126
|
+
}
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const skillFolder = join(dirPath, entry.name);
|
|
131
|
+
const skillFilePath = join(skillFolder, SKILL_FILE_NAME);
|
|
132
|
+
let skillStat;
|
|
133
|
+
try {
|
|
134
|
+
skillStat = await lstat(skillFilePath);
|
|
135
|
+
} catch (_error) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (skillStat.isSymbolicLink()) {
|
|
140
|
+
if (this.debug) {
|
|
141
|
+
console.warn(`[skills] Skipping symlinked SKILL.md: ${skillFilePath}`);
|
|
142
|
+
}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const resolvedSkillPath = await this._resolveRealPath(skillFilePath);
|
|
147
|
+
if (!resolvedSkillPath || !isPathInside(dirPath, resolvedSkillPath)) {
|
|
148
|
+
if (this.debug) {
|
|
149
|
+
console.warn(`[skills] Skipping skill path outside directory: ${resolvedSkillPath || skillFilePath}`);
|
|
150
|
+
}
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (!existsSync(skillFilePath)) continue;
|
|
154
|
+
|
|
155
|
+
const { skill, error } = await parseSkillFile(skillFilePath, entry.name);
|
|
156
|
+
if (!skill) {
|
|
157
|
+
if (error) {
|
|
158
|
+
this.loadErrors.push({
|
|
159
|
+
path: skillFilePath,
|
|
160
|
+
code: error.code,
|
|
161
|
+
message: error.message
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
if (this.debug && error) {
|
|
165
|
+
console.warn(`[skills] Skipping ${skillFilePath}: ${error.code} (${error.message})`);
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (this.skillsByName.has(skill.name)) {
|
|
171
|
+
if (this.debug) {
|
|
172
|
+
console.warn(`[skills] Duplicate skill name '${skill.name}' at ${skillFolder}, skipping`);
|
|
173
|
+
}
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this.skillsByName.set(skill.name, skill);
|
|
178
|
+
results.push(skill);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return results;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export { DEFAULT_SKILL_DIRS };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { wrapToolWithEmitter } from '../probeTool.js';
|
|
2
|
+
|
|
3
|
+
function normalizeSkillName(name) {
|
|
4
|
+
return typeof name === 'string' ? name.trim() : '';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function createSkillToolInstances({ registry, activeSkills }) {
|
|
8
|
+
const listSkillsTool = {
|
|
9
|
+
execute: async (params = {}) => {
|
|
10
|
+
const filter = typeof params.filter === 'string' ? params.filter.trim().toLowerCase() : '';
|
|
11
|
+
const skills = await registry.loadSkills();
|
|
12
|
+
const filtered = filter
|
|
13
|
+
? skills.filter(skill =>
|
|
14
|
+
skill.name.toLowerCase().includes(filter) ||
|
|
15
|
+
(skill.description || '').toLowerCase().includes(filter)
|
|
16
|
+
)
|
|
17
|
+
: skills;
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
skills: filtered.map(skill => ({
|
|
21
|
+
name: skill.name,
|
|
22
|
+
description: skill.description
|
|
23
|
+
}))
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const useSkillTool = {
|
|
29
|
+
execute: async (params = {}) => {
|
|
30
|
+
const rawName = normalizeSkillName(params.name);
|
|
31
|
+
if (!rawName) {
|
|
32
|
+
throw new Error('Skill name is required');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
await registry.loadSkills();
|
|
36
|
+
let skill = registry.getSkill(rawName);
|
|
37
|
+
if (!skill) {
|
|
38
|
+
skill = registry.getSkill(rawName.toLowerCase());
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!skill) {
|
|
42
|
+
const available = registry.getSkills().map(s => s.name).join(', ') || 'None';
|
|
43
|
+
throw new Error(`Skill '${rawName}' not found. Available skills: ${available}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const instructions = await registry.loadSkillInstructions(skill.name);
|
|
47
|
+
if (!instructions) {
|
|
48
|
+
throw new Error(`Skill '${skill.name}' has no instructions`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
activeSkills.set(skill.name, { ...skill, instructions });
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
name: skill.name,
|
|
55
|
+
description: skill.description,
|
|
56
|
+
instructions
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
listSkillsToolInstance: wrapToolWithEmitter(listSkillsTool, 'listSkills', listSkillsTool.execute),
|
|
63
|
+
useSkillToolInstance: wrapToolWithEmitter(useSkillTool, 'useSkill', useSkillTool.execute)
|
|
64
|
+
};
|
|
65
|
+
}
|
package/build/agent/tools.js
CHANGED
|
@@ -172,6 +172,50 @@ User: Find all markdown files in the docs directory, but only at the top level.
|
|
|
172
172
|
</examples>
|
|
173
173
|
`;
|
|
174
174
|
|
|
175
|
+
// Define the listSkills tool XML definition
|
|
176
|
+
export const listSkillsToolDefinition = `
|
|
177
|
+
## listSkills
|
|
178
|
+
Description: List available agent skills discovered in the repository.
|
|
179
|
+
|
|
180
|
+
Parameters:
|
|
181
|
+
- filter: (optional) Substring filter to match skill names or descriptions.
|
|
182
|
+
|
|
183
|
+
Usage Example:
|
|
184
|
+
|
|
185
|
+
<examples>
|
|
186
|
+
|
|
187
|
+
User: What skills are available?
|
|
188
|
+
<listSkills>
|
|
189
|
+
</listSkills>
|
|
190
|
+
|
|
191
|
+
User: Show me skills related to docs
|
|
192
|
+
<listSkills>
|
|
193
|
+
<filter>docs</filter>
|
|
194
|
+
</listSkills>
|
|
195
|
+
|
|
196
|
+
</examples>
|
|
197
|
+
`;
|
|
198
|
+
|
|
199
|
+
// Define the useSkill tool XML definition
|
|
200
|
+
export const useSkillToolDefinition = `
|
|
201
|
+
## useSkill
|
|
202
|
+
Description: Load and activate a specific skill's instructions. Use this before following a skill's guidance.
|
|
203
|
+
|
|
204
|
+
Parameters:
|
|
205
|
+
- name: (required) The skill name to activate.
|
|
206
|
+
|
|
207
|
+
Usage Example:
|
|
208
|
+
|
|
209
|
+
<examples>
|
|
210
|
+
|
|
211
|
+
User: Use the onboarding skill
|
|
212
|
+
<useSkill>
|
|
213
|
+
<name>onboarding</name>
|
|
214
|
+
</useSkill>
|
|
215
|
+
|
|
216
|
+
</examples>
|
|
217
|
+
`;
|
|
218
|
+
|
|
175
219
|
// Define the readImage tool XML definition
|
|
176
220
|
export const readImageToolDefinition = `
|
|
177
221
|
## readImage
|