@probelabs/probe 0.6.0-rc201 → 0.6.0-rc203

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 (46) hide show
  1. package/README.md +31 -1
  2. package/bin/binaries/probe-v0.6.0-rc203-aarch64-apple-darwin.tar.gz +0 -0
  3. package/bin/binaries/probe-v0.6.0-rc203-aarch64-unknown-linux-musl.tar.gz +0 -0
  4. package/bin/binaries/probe-v0.6.0-rc203-x86_64-apple-darwin.tar.gz +0 -0
  5. package/bin/binaries/probe-v0.6.0-rc203-x86_64-pc-windows-msvc.zip +0 -0
  6. package/bin/binaries/probe-v0.6.0-rc203-x86_64-unknown-linux-musl.tar.gz +0 -0
  7. package/build/agent/ProbeAgent.d.ts +11 -1
  8. package/build/agent/ProbeAgent.js +310 -14
  9. package/build/agent/index.js +8615 -394
  10. package/build/agent/probeTool.js +2 -2
  11. package/build/agent/schemaUtils.js +37 -18
  12. package/build/agent/shared/prompts.js +17 -0
  13. package/build/agent/skills/formatting.js +23 -0
  14. package/build/agent/skills/parser.js +162 -0
  15. package/build/agent/skills/registry.js +185 -0
  16. package/build/agent/skills/tools.js +65 -0
  17. package/build/agent/tools.js +44 -0
  18. package/build/delegate.js +27 -7
  19. package/build/tools/common.js +17 -4
  20. package/build/tools/system-message.js +4 -4
  21. package/build/tools/vercel.js +243 -36
  22. package/cjs/agent/ProbeAgent.cjs +14990 -7892
  23. package/cjs/index.cjs +15003 -7905
  24. package/index.d.ts +8 -0
  25. package/package.json +2 -1
  26. package/scripts/postinstall.js +10 -4
  27. package/src/agent/ProbeAgent.d.ts +11 -1
  28. package/src/agent/ProbeAgent.js +310 -14
  29. package/src/agent/index.js +21 -1
  30. package/src/agent/probeTool.js +2 -2
  31. package/src/agent/schemaUtils.js +37 -18
  32. package/src/agent/shared/prompts.js +17 -0
  33. package/src/agent/skills/formatting.js +23 -0
  34. package/src/agent/skills/parser.js +162 -0
  35. package/src/agent/skills/registry.js +185 -0
  36. package/src/agent/skills/tools.js +65 -0
  37. package/src/agent/tools.js +44 -0
  38. package/src/delegate.js +27 -7
  39. package/src/tools/common.js +17 -4
  40. package/src/tools/system-message.js +4 -4
  41. package/src/tools/vercel.js +243 -36
  42. package/bin/binaries/probe-v0.6.0-rc201-aarch64-apple-darwin.tar.gz +0 -0
  43. package/bin/binaries/probe-v0.6.0-rc201-aarch64-unknown-linux-musl.tar.gz +0 -0
  44. package/bin/binaries/probe-v0.6.0-rc201-x86_64-apple-darwin.tar.gz +0 -0
  45. package/bin/binaries/probe-v0.6.0-rc201-x86_64-pc-windows-msvc.zip +0 -0
  46. package/bin/binaries/probe-v0.6.0-rc201-x86_64-unknown-linux-musl.tar.gz +0 -0
@@ -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
  };
@@ -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
- const attributesStr = originalDiagram.attributes ? ` ${originalDiagram.attributes}` : '';
1853
- const newCodeBlock = `\`\`\`mermaid${attributesStr}\n${maidResult.fixed}\n\`\`\``;
1854
-
1855
- fixedResponse = fixedResponse.slice(0, originalDiagram.startIndex) +
1856
- newCodeBlock +
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
- const attributesStr = originalDiagram.attributes ? ` ${originalDiagram.attributes}` : '';
1878
- const newCodeBlock = `\`\`\`mermaid${attributesStr}\n${maidResult.fixed}\n\`\`\``;
1879
-
1880
- fixedResponse = fixedResponse.slice(0, originalDiagram.startIndex) +
1881
- newCodeBlock +
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
- const attributesStr = originalDiagram.attributes ? ` ${originalDiagram.attributes}` : '';
1991
- const newCodeBlock = `\`\`\`mermaid${attributesStr}\n${fixedContent}\n\`\`\``;
1992
-
1993
- fixedResponse = fixedResponse.slice(0, originalDiagram.startIndex) +
1994
- newCodeBlock +
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, '&amp;')
4
+ .replace(/</g, '&lt;')
5
+ .replace(/>/g, '&gt;')
6
+ .replace(/"/g, '&quot;')
7
+ .replace(/'/g, '&apos;');
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
+ }