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/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
+ }