mentat-mcp 1.0.0
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 +122 -0
- package/dist/index.js +353 -0
- package/dist/setup.js +209 -0
- package/dist/skills/engine.js +362 -0
- package/dist/skills.js +141 -0
- package/package.json +48 -0
- package/src/index.ts +426 -0
- package/src/setup.ts +249 -0
- package/src/skills.ts +199 -0
- package/tsconfig.json +16 -0
package/src/skills.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import yaml from 'yaml';
|
|
4
|
+
import { glob } from 'glob';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Skills = curated prompt library for Claude
|
|
8
|
+
* This module just loads YAML and formats instructions + context
|
|
9
|
+
* Claude does all the actual work using its Edit/Read/Bash tools
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface SkillDefinition {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
instructions: string;
|
|
17
|
+
context_patterns?: string[];
|
|
18
|
+
examples?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface GatheredContext {
|
|
22
|
+
files: Array<{ path: string; content: string; lines: number }>;
|
|
23
|
+
totalChars: number;
|
|
24
|
+
truncated: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const LIMITS = {
|
|
28
|
+
maxFiles: 10,
|
|
29
|
+
maxFileSize: 50_000,
|
|
30
|
+
maxTotalChars: 400_000,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export class SkillLibrary {
|
|
34
|
+
constructor(
|
|
35
|
+
private skillsPath: string,
|
|
36
|
+
private workspacePath: string
|
|
37
|
+
) {}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Load skill from YAML
|
|
41
|
+
*/
|
|
42
|
+
async loadSkill(skillId: string): Promise<SkillDefinition> {
|
|
43
|
+
const skillPath = path.join(this.skillsPath, `${skillId}.yaml`);
|
|
44
|
+
const content = await fs.readFile(skillPath, 'utf-8');
|
|
45
|
+
const parsed = yaml.parse(content);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
id: parsed.skill.id,
|
|
49
|
+
name: parsed.skill.name,
|
|
50
|
+
description: parsed.skill.description,
|
|
51
|
+
instructions: parsed.skill.instructions || this.generateInstructions(parsed.skill),
|
|
52
|
+
context_patterns: parsed.skill.context_patterns,
|
|
53
|
+
examples: parsed.skill.examples,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Gather file context based on patterns or explicit files
|
|
59
|
+
*/
|
|
60
|
+
async gatherContext(
|
|
61
|
+
patterns: string[] = [],
|
|
62
|
+
explicitFiles: string[] = []
|
|
63
|
+
): Promise<GatheredContext> {
|
|
64
|
+
const filePaths = new Set<string>();
|
|
65
|
+
|
|
66
|
+
// Add explicit files
|
|
67
|
+
explicitFiles.forEach((f) => filePaths.add(path.resolve(this.workspacePath, f)));
|
|
68
|
+
|
|
69
|
+
// Add files matching patterns
|
|
70
|
+
for (const pattern of patterns) {
|
|
71
|
+
const matches = await glob(pattern, {
|
|
72
|
+
cwd: this.workspacePath,
|
|
73
|
+
absolute: true,
|
|
74
|
+
ignore: ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**'],
|
|
75
|
+
});
|
|
76
|
+
matches.slice(0, LIMITS.maxFiles).forEach((f) => filePaths.add(f));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Rank and limit files
|
|
80
|
+
const rankedFiles = await this.rankFiles(Array.from(filePaths));
|
|
81
|
+
const limitedFiles = rankedFiles.slice(0, LIMITS.maxFiles);
|
|
82
|
+
|
|
83
|
+
// Read file contents
|
|
84
|
+
const files = [];
|
|
85
|
+
let totalChars = 0;
|
|
86
|
+
let truncated = false;
|
|
87
|
+
|
|
88
|
+
for (const filePath of limitedFiles) {
|
|
89
|
+
try {
|
|
90
|
+
const stats = await fs.stat(filePath);
|
|
91
|
+
|
|
92
|
+
if (stats.size > LIMITS.maxFileSize) {
|
|
93
|
+
truncated = true;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (totalChars + stats.size > LIMITS.maxTotalChars) {
|
|
98
|
+
truncated = true;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
103
|
+
const lines = content.split('\n').length;
|
|
104
|
+
|
|
105
|
+
files.push({
|
|
106
|
+
path: path.relative(this.workspacePath, filePath),
|
|
107
|
+
content,
|
|
108
|
+
lines,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
totalChars += content.length;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error(`Failed to read ${filePath}:`, error);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { files, totalChars, truncated };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Rank files by relevance
|
|
122
|
+
*/
|
|
123
|
+
private async rankFiles(files: string[]): Promise<string[]> {
|
|
124
|
+
const scored = await Promise.all(
|
|
125
|
+
files.map(async (f) => {
|
|
126
|
+
const stats = await fs.stat(f).catch(() => null);
|
|
127
|
+
const relativePath = path.relative(this.workspacePath, f);
|
|
128
|
+
|
|
129
|
+
let score = 0;
|
|
130
|
+
|
|
131
|
+
// Deprioritize test files
|
|
132
|
+
if (relativePath.includes('test') || relativePath.includes('spec')) {
|
|
133
|
+
score -= 100;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Prefer root-level files
|
|
137
|
+
const depth = relativePath.split(path.sep).length;
|
|
138
|
+
score -= depth * 10;
|
|
139
|
+
|
|
140
|
+
// Slight preference for smaller files
|
|
141
|
+
if (stats) {
|
|
142
|
+
score -= stats.size / 10000;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { path: f, score };
|
|
146
|
+
})
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
return scored.sort((a, b) => b.score - a.score).map((s) => s.path);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Format skill + context for Claude to read
|
|
154
|
+
*/
|
|
155
|
+
formatForClaude(skill: SkillDefinition, context: GatheredContext): string {
|
|
156
|
+
const parts = [];
|
|
157
|
+
|
|
158
|
+
parts.push(`# ${skill.name}`);
|
|
159
|
+
parts.push(`${skill.description}\n`);
|
|
160
|
+
|
|
161
|
+
parts.push(`## Instructions`);
|
|
162
|
+
parts.push(skill.instructions);
|
|
163
|
+
parts.push('');
|
|
164
|
+
|
|
165
|
+
if (skill.examples) {
|
|
166
|
+
parts.push(`## Examples`);
|
|
167
|
+
parts.push(skill.examples);
|
|
168
|
+
parts.push('');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (context.files.length > 0) {
|
|
172
|
+
parts.push(`## Context (${context.files.length} file${context.files.length > 1 ? 's' : ''})`);
|
|
173
|
+
|
|
174
|
+
for (const file of context.files) {
|
|
175
|
+
parts.push(`\n### ${file.path} (${file.lines} lines)`);
|
|
176
|
+
parts.push('```');
|
|
177
|
+
parts.push(file.content);
|
|
178
|
+
parts.push('```');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (context.truncated) {
|
|
182
|
+
parts.push(`\n⚠️ Some files were excluded due to size limits.`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
parts.push(`\n---`);
|
|
187
|
+
parts.push(`Use your Edit tool to make changes.`);
|
|
188
|
+
parts.push(`Use Read tool if you need additional files.`);
|
|
189
|
+
|
|
190
|
+
return parts.join('\n');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Backward compatibility for old YAML format
|
|
195
|
+
*/
|
|
196
|
+
private generateInstructions(skillDef: any): string {
|
|
197
|
+
return skillDef.description || 'Complete the task as described.';
|
|
198
|
+
}
|
|
199
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"],
|
|
15
|
+
"exclude": ["node_modules", "dist"]
|
|
16
|
+
}
|