@probelabs/probe 0.6.0-rc201 → 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 +11 -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 +14990 -7892
- package/cjs/index.cjs +15003 -7905
- package/index.d.ts +8 -0
- package/package.json +2 -1
- package/scripts/postinstall.js +10 -4
- package/src/agent/ProbeAgent.d.ts +11 -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-rc201-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc201-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc201-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc201-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc201-x86_64-unknown-linux-musl.tar.gz +0 -0
package/src/agent/index.js
CHANGED
|
@@ -120,6 +120,7 @@ function parseArgs() {
|
|
|
120
120
|
allowedFolders: null,
|
|
121
121
|
prompt: null,
|
|
122
122
|
systemPrompt: null,
|
|
123
|
+
architectureFileName: null,
|
|
123
124
|
schema: null,
|
|
124
125
|
provider: null,
|
|
125
126
|
model: null,
|
|
@@ -137,6 +138,8 @@ function parseArgs() {
|
|
|
137
138
|
noMermaidValidation: false, // New flag to disable mermaid validation
|
|
138
139
|
allowedTools: null, // Tool filtering: ['*'] = all, [] = none, ['tool1', 'tool2'] = specific
|
|
139
140
|
disableTools: false, // Convenience flag to disable all tools
|
|
141
|
+
disableSkills: false, // Disable skill discovery and activation
|
|
142
|
+
skillDirs: null, // Comma-separated list of repo-relative skill directories
|
|
140
143
|
// Bash tool configuration
|
|
141
144
|
enableBash: false,
|
|
142
145
|
bashAllow: null,
|
|
@@ -172,6 +175,8 @@ function parseArgs() {
|
|
|
172
175
|
config.prompt = args[++i];
|
|
173
176
|
} else if (arg === '--system-prompt' && i + 1 < args.length) {
|
|
174
177
|
config.systemPrompt = args[++i];
|
|
178
|
+
} else if (arg === '--architecture-file' && i + 1 < args.length) {
|
|
179
|
+
config.architectureFileName = args[++i];
|
|
175
180
|
} else if (arg === '--schema' && i + 1 < args.length) {
|
|
176
181
|
config.schema = args[++i];
|
|
177
182
|
} else if (arg === '--provider' && i + 1 < args.length) {
|
|
@@ -205,6 +210,10 @@ function parseArgs() {
|
|
|
205
210
|
} else if (arg === '--disable-tools') {
|
|
206
211
|
// Convenience flag to disable all tools (raw AI mode)
|
|
207
212
|
config.disableTools = true;
|
|
213
|
+
} else if (arg === '--no-skills') {
|
|
214
|
+
config.disableSkills = true;
|
|
215
|
+
} else if (arg === '--skills-dir' && i + 1 < args.length) {
|
|
216
|
+
config.skillDirs = args[++i].split(',').map(dir => dir.trim()).filter(Boolean);
|
|
208
217
|
} else if (arg === '--enable-bash') {
|
|
209
218
|
config.enableBash = true;
|
|
210
219
|
} else if (arg === '--bash-allow' && i + 1 < args.length) {
|
|
@@ -254,6 +263,7 @@ Options:
|
|
|
254
263
|
--allowed-folders <dirs> Comma-separated list of allowed directories for file operations
|
|
255
264
|
--prompt <type> Persona: code-explorer, engineer, code-review, support, architect
|
|
256
265
|
--system-prompt <text|file> Custom system prompt (text or file path)
|
|
266
|
+
--architecture-file <name> Architecture context filename in repo root (defaults to AGENTS.md with CLAUDE.md fallback; ARCHITECTURE.md is always included when present)
|
|
257
267
|
--schema <schema|file> Output schema (JSON, XML, any format - text or file path)
|
|
258
268
|
--provider <name> Force AI provider: anthropic, openai, google
|
|
259
269
|
--model <name> Override model name
|
|
@@ -262,10 +272,12 @@ Options:
|
|
|
262
272
|
--allowed-tools <tools> Filter available tools (comma-separated list)
|
|
263
273
|
Use '*' or 'all' for all tools (default)
|
|
264
274
|
Use 'none' or '' for no tools (raw AI mode)
|
|
265
|
-
Specific tools: search,query,extract,listFiles,searchFiles
|
|
275
|
+
Specific tools: search,query,extract,listFiles,searchFiles,listSkills,useSkill
|
|
266
276
|
Supports exclusion: '*,!bash' (all except bash)
|
|
267
277
|
--disable-tools Disable all tools (raw AI mode, no code analysis)
|
|
268
278
|
Convenience flag equivalent to --allowed-tools none
|
|
279
|
+
--skills-dir <dirs> Comma-separated list of repo-relative skill directories to scan
|
|
280
|
+
--no-skills Disable skill discovery and activation
|
|
269
281
|
--verbose Enable verbose output
|
|
270
282
|
--outline Use outline-xml format for code search results
|
|
271
283
|
--mcp Run as MCP server
|
|
@@ -391,6 +403,10 @@ class ProbeAgentMcpServer {
|
|
|
391
403
|
system_prompt: {
|
|
392
404
|
type: 'string',
|
|
393
405
|
description: 'Optional custom system prompt (text or file path).',
|
|
406
|
+
},
|
|
407
|
+
architecture_file: {
|
|
408
|
+
type: 'string',
|
|
409
|
+
description: 'Optional architecture context filename in repo root (defaults to AGENTS.md with CLAUDE.md fallback; ARCHITECTURE.md is always included when present).',
|
|
394
410
|
}
|
|
395
411
|
},
|
|
396
412
|
required: ['query']
|
|
@@ -511,6 +527,7 @@ class ProbeAgentMcpServer {
|
|
|
511
527
|
path: args.path || (args.allowed_folders && args.allowed_folders[0]) || process.cwd(),
|
|
512
528
|
promptType: args.prompt || 'code-explorer',
|
|
513
529
|
customPrompt: systemPrompt,
|
|
530
|
+
architectureFileName: args.architecture_file,
|
|
514
531
|
provider: args.provider,
|
|
515
532
|
model: args.model,
|
|
516
533
|
allowEdit: !!args.allow_edit,
|
|
@@ -803,6 +820,7 @@ async function main() {
|
|
|
803
820
|
allowedFolders: config.allowedFolders,
|
|
804
821
|
promptType: config.prompt,
|
|
805
822
|
customPrompt: systemPrompt,
|
|
823
|
+
architectureFileName: config.architectureFileName,
|
|
806
824
|
allowEdit: config.allowEdit,
|
|
807
825
|
enableDelegate: config.enableDelegate,
|
|
808
826
|
debug: config.verbose,
|
|
@@ -812,6 +830,8 @@ async function main() {
|
|
|
812
830
|
disableMermaidValidation: config.noMermaidValidation,
|
|
813
831
|
allowedTools: config.allowedTools,
|
|
814
832
|
disableTools: config.disableTools,
|
|
833
|
+
enableSkills: !config.disableSkills,
|
|
834
|
+
skillDirs: config.skillDirs,
|
|
815
835
|
enableBash: config.enableBash,
|
|
816
836
|
bashConfig: bashConfig
|
|
817
837
|
};
|
package/src/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);
|
package/src/agent/schemaUtils.js
CHANGED
|
@@ -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
|
+
}
|