@pheem49/mint 1.5.1 → 1.5.2
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 +8 -0
- package/mint-cli.js +148 -921
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json +31 -1
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json +6 -1
- package/package.json +18 -20
- package/src/AI_Brain/proactive_engine.js +12 -2
- package/src/Automation_Layer/browser_automation.js +26 -24
- package/src/CLI/approval_handler.js +42 -0
- package/src/CLI/chat_ui.js +192 -7
- package/src/CLI/cli_colors.js +32 -0
- package/src/CLI/cli_formatters.js +89 -0
- package/src/CLI/code_agent.js +166 -57
- package/src/CLI/intent_detectors.js +181 -0
- package/src/CLI/interactive_chat.js +479 -0
- package/src/CLI/list_features.js +3 -0
- package/src/CLI/repo_summarizer.js +282 -0
- package/src/CLI/semantic_code_search.js +312 -0
- package/src/CLI/skill_manager.js +41 -0
- package/src/CLI/slash_command_handler.js +418 -0
- package/src/CLI/symbol_indexer.js +231 -0
- package/src/Channels/discord_bridge.js +11 -13
- package/src/Channels/line_bridge.js +10 -10
- package/src/Channels/slack_bridge.js +7 -12
- package/src/Channels/telegram_bridge.js +6 -14
- package/src/Channels/whatsapp_bridge.js +11 -9
- package/src/System/chat_history_manager.js +20 -12
- package/src/System/optional_require.js +23 -0
- package/src/UI/live2d_manager.js +211 -13
- package/src/UI/renderer.js +163 -3
- package/src/UI/settings.css +655 -420
- package/src/UI/settings.html +478 -432
- package/src/UI/settings.js +10 -8
- package/src/UI/styles.css +89 -25
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execFileSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
const IGNORED_DIRS = new Set([
|
|
6
|
+
'.git',
|
|
7
|
+
'.cache',
|
|
8
|
+
'.next',
|
|
9
|
+
'.nuxt',
|
|
10
|
+
'coverage',
|
|
11
|
+
'dist',
|
|
12
|
+
'build',
|
|
13
|
+
'out',
|
|
14
|
+
'node_modules'
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
const LANGUAGE_BY_EXT = {
|
|
18
|
+
'.cjs': 'JavaScript',
|
|
19
|
+
'.css': 'CSS',
|
|
20
|
+
'.html': 'HTML',
|
|
21
|
+
'.js': 'JavaScript',
|
|
22
|
+
'.json': 'JSON',
|
|
23
|
+
'.jsx': 'JavaScript',
|
|
24
|
+
'.md': 'Markdown',
|
|
25
|
+
'.mjs': 'JavaScript',
|
|
26
|
+
'.py': 'Python',
|
|
27
|
+
'.rs': 'Rust',
|
|
28
|
+
'.sh': 'Shell',
|
|
29
|
+
'.ts': 'TypeScript',
|
|
30
|
+
'.tsx': 'TypeScript',
|
|
31
|
+
'.vue': 'Vue',
|
|
32
|
+
'.yaml': 'YAML',
|
|
33
|
+
'.yml': 'YAML'
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function safeReadJson(filePath) {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
39
|
+
} catch (_) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function safeGit(root, args) {
|
|
45
|
+
try {
|
|
46
|
+
return execFileSync('git', args, {
|
|
47
|
+
cwd: root,
|
|
48
|
+
encoding: 'utf8',
|
|
49
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
50
|
+
maxBuffer: 1024 * 1024
|
|
51
|
+
}).trim();
|
|
52
|
+
} catch (_) {
|
|
53
|
+
return '';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function walkFiles(root, options = {}) {
|
|
58
|
+
const maxFiles = options.maxFiles || 2500;
|
|
59
|
+
const files = [];
|
|
60
|
+
|
|
61
|
+
function visit(dir) {
|
|
62
|
+
let entries = [];
|
|
63
|
+
try {
|
|
64
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
65
|
+
} catch (_) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
if (files.length >= maxFiles) return;
|
|
72
|
+
const fullPath = path.join(dir, entry.name);
|
|
73
|
+
const relativePath = path.relative(root, fullPath);
|
|
74
|
+
|
|
75
|
+
if (entry.isDirectory()) {
|
|
76
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
77
|
+
visit(fullPath);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (entry.isFile()) {
|
|
82
|
+
files.push(relativePath);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
visit(root);
|
|
88
|
+
return files;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function summarizeLanguages(files) {
|
|
92
|
+
const counts = new Map();
|
|
93
|
+
for (const file of files) {
|
|
94
|
+
const language = LANGUAGE_BY_EXT[path.extname(file).toLowerCase()] || 'Other';
|
|
95
|
+
counts.set(language, (counts.get(language) || 0) + 1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return Array.from(counts.entries())
|
|
99
|
+
.map(([language, count]) => ({ language, count }))
|
|
100
|
+
.sort((a, b) => b.count - a.count || a.language.localeCompare(b.language));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function findImportantFiles(files) {
|
|
104
|
+
const importantPatterns = [
|
|
105
|
+
/^README/i,
|
|
106
|
+
/^package\.json$/,
|
|
107
|
+
/^RELEASE_NOTES\.md$/,
|
|
108
|
+
/^CHANGELOG/i,
|
|
109
|
+
/^Dockerfile$/,
|
|
110
|
+
/^docker-compose\./,
|
|
111
|
+
/^\.github\//,
|
|
112
|
+
/^src\//,
|
|
113
|
+
/^tests?\//,
|
|
114
|
+
/config/i,
|
|
115
|
+
/\.(test|spec)\.[cm]?[jt]sx?$/
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
return files
|
|
119
|
+
.filter(file => importantPatterns.some(pattern => pattern.test(file)))
|
|
120
|
+
.slice(0, 30);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function summarizeTopDirs(files) {
|
|
124
|
+
const counts = new Map();
|
|
125
|
+
for (const file of files) {
|
|
126
|
+
const top = file.includes(path.sep) ? file.split(path.sep)[0] : '(root)';
|
|
127
|
+
counts.set(top, (counts.get(top) || 0) + 1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return Array.from(counts.entries())
|
|
131
|
+
.map(([dir, count]) => ({ dir, count }))
|
|
132
|
+
.sort((a, b) => b.count - a.count || a.dir.localeCompare(b.dir))
|
|
133
|
+
.slice(0, 12);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function summarizePackage(root) {
|
|
137
|
+
const pkg = safeReadJson(path.join(root, 'package.json'));
|
|
138
|
+
if (!pkg) return null;
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
name: pkg.name || '',
|
|
142
|
+
version: pkg.version || '',
|
|
143
|
+
description: pkg.description || '',
|
|
144
|
+
scripts: Object.keys(pkg.scripts || {}),
|
|
145
|
+
dependencies: Object.keys(pkg.dependencies || {}),
|
|
146
|
+
devDependencies: Object.keys(pkg.devDependencies || {})
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function summarizeGit(root) {
|
|
151
|
+
const inside = safeGit(root, ['rev-parse', '--is-inside-work-tree']) === 'true';
|
|
152
|
+
if (!inside) {
|
|
153
|
+
return { isRepo: false };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
isRepo: true,
|
|
158
|
+
branch: safeGit(root, ['branch', '--show-current']) || '(detached HEAD)',
|
|
159
|
+
status: safeGit(root, ['status', '--short']) || '(clean)',
|
|
160
|
+
diffStat: safeGit(root, ['diff', '--stat']) || '(no unstaged diff)'
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function summarizeRepository(targetPath = process.cwd(), options = {}) {
|
|
165
|
+
const root = path.resolve(targetPath);
|
|
166
|
+
const stat = fs.statSync(root);
|
|
167
|
+
if (!stat.isDirectory()) {
|
|
168
|
+
throw new Error(`Repository path is not a directory: ${root}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const files = walkFiles(root, options);
|
|
172
|
+
return {
|
|
173
|
+
root,
|
|
174
|
+
fileCount: files.length,
|
|
175
|
+
topDirs: summarizeTopDirs(files),
|
|
176
|
+
languages: summarizeLanguages(files),
|
|
177
|
+
importantFiles: findImportantFiles(files),
|
|
178
|
+
package: summarizePackage(root),
|
|
179
|
+
git: summarizeGit(root)
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function formatList(items, formatter, emptyText = '(none)') {
|
|
184
|
+
if (!Array.isArray(items) || items.length === 0) return `- ${emptyText}`;
|
|
185
|
+
return items.map(formatter).join('\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function formatMultilineField(label, value, emptyText) {
|
|
189
|
+
const text = value || emptyText;
|
|
190
|
+
const lines = String(text).split('\n');
|
|
191
|
+
if (lines.length === 1) {
|
|
192
|
+
return `- ${label}: ${lines[0]}`;
|
|
193
|
+
}
|
|
194
|
+
return [
|
|
195
|
+
`- ${label}:`,
|
|
196
|
+
...lines.map(line => ` ${line}`)
|
|
197
|
+
].join('\n');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function formatRepoSummary(summary) {
|
|
201
|
+
const pkg = summary.package;
|
|
202
|
+
const git = summary.git;
|
|
203
|
+
const lines = [];
|
|
204
|
+
|
|
205
|
+
lines.push(`# Repository Summary`);
|
|
206
|
+
lines.push('');
|
|
207
|
+
lines.push(`Root: ${summary.root}`);
|
|
208
|
+
if (pkg?.name) {
|
|
209
|
+
lines.push(`Package: ${pkg.name}${pkg.version ? ` v${pkg.version}` : ''}`);
|
|
210
|
+
}
|
|
211
|
+
if (pkg?.description) {
|
|
212
|
+
lines.push(`Description: ${pkg.description}`);
|
|
213
|
+
lines.push('');
|
|
214
|
+
}
|
|
215
|
+
lines.push(`Files scanned: ${summary.fileCount}`);
|
|
216
|
+
|
|
217
|
+
lines.push('');
|
|
218
|
+
lines.push(`## Git`);
|
|
219
|
+
if (!git?.isRepo) {
|
|
220
|
+
lines.push('- Not a git repository');
|
|
221
|
+
} else {
|
|
222
|
+
lines.push(`- Branch: ${git.branch}`);
|
|
223
|
+
lines.push(formatMultilineField('Status', git.status, '(clean)'));
|
|
224
|
+
lines.push(formatMultilineField('Diff', git.diffStat, '(no unstaged diff)'));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
lines.push('');
|
|
228
|
+
lines.push(`## Top Directories`);
|
|
229
|
+
lines.push(formatList(
|
|
230
|
+
summary.topDirs,
|
|
231
|
+
item => `- ${item.dir}: ${item.count} file(s)`
|
|
232
|
+
));
|
|
233
|
+
|
|
234
|
+
lines.push('');
|
|
235
|
+
lines.push(`## Languages`);
|
|
236
|
+
lines.push(formatList(
|
|
237
|
+
summary.languages.slice(0, 10),
|
|
238
|
+
item => `- ${item.language}: ${item.count} file(s)`
|
|
239
|
+
));
|
|
240
|
+
|
|
241
|
+
lines.push('');
|
|
242
|
+
lines.push(`## Package Scripts`);
|
|
243
|
+
lines.push(formatList(
|
|
244
|
+
pkg?.scripts || [],
|
|
245
|
+
script => `- ${script}`
|
|
246
|
+
));
|
|
247
|
+
|
|
248
|
+
lines.push('');
|
|
249
|
+
lines.push(`## Dependencies`);
|
|
250
|
+
if (pkg) {
|
|
251
|
+
const depCount = pkg.dependencies.length;
|
|
252
|
+
const devDepCount = pkg.devDependencies.length;
|
|
253
|
+
lines.push(`- Runtime: ${depCount}`);
|
|
254
|
+
lines.push(`- Development: ${devDepCount}`);
|
|
255
|
+
const notable = pkg.dependencies.slice(0, 12);
|
|
256
|
+
if (notable.length > 0) {
|
|
257
|
+
lines.push(`- Notable runtime deps: ${notable.join(', ')}`);
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
lines.push('- No package.json found');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
lines.push('');
|
|
264
|
+
lines.push(`## Important Files`);
|
|
265
|
+
lines.push(formatList(
|
|
266
|
+
summary.importantFiles,
|
|
267
|
+
file => `- ${file}`
|
|
268
|
+
));
|
|
269
|
+
|
|
270
|
+
return lines.join('\n');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
module.exports = {
|
|
274
|
+
summarizeRepository,
|
|
275
|
+
formatRepoSummary,
|
|
276
|
+
_helpers: {
|
|
277
|
+
walkFiles,
|
|
278
|
+
summarizeLanguages,
|
|
279
|
+
summarizeTopDirs,
|
|
280
|
+
findImportantFiles
|
|
281
|
+
}
|
|
282
|
+
};
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const { GoogleGenAI } = require('@google/genai');
|
|
6
|
+
const { readConfig } = require('../System/config_manager');
|
|
7
|
+
const { _helpers: symbolHelpers } = require('./symbol_indexer');
|
|
8
|
+
|
|
9
|
+
const DEFAULT_EMBEDDING_MODEL = 'gemini-embedding-001';
|
|
10
|
+
const DEFAULT_MAX_CHARS = 1800;
|
|
11
|
+
const DEFAULT_OVERLAP_LINES = 8;
|
|
12
|
+
const DEFAULT_MAX_FILE_BYTES = 512 * 1024;
|
|
13
|
+
|
|
14
|
+
function getStoreDir(options = {}) {
|
|
15
|
+
const dir = path.join(os.homedir(), '.config', 'mint', 'semantic-code');
|
|
16
|
+
if (options.create) {
|
|
17
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
return dir;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getWorkspaceStorePath(root, options = {}) {
|
|
23
|
+
const hash = crypto.createHash('sha1').update(path.resolve(root)).digest('hex').slice(0, 16);
|
|
24
|
+
return path.join(getStoreDir(options), `${hash}.json`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resolveApiKey() {
|
|
28
|
+
let settingsKey = '';
|
|
29
|
+
try {
|
|
30
|
+
settingsKey = (readConfig().apiKey || '').trim();
|
|
31
|
+
} catch (_) {
|
|
32
|
+
settingsKey = '';
|
|
33
|
+
}
|
|
34
|
+
return settingsKey || process.env.GEMINI_API_KEY || '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function defaultEmbedText(text, model = DEFAULT_EMBEDDING_MODEL) {
|
|
38
|
+
const apiKey = resolveApiKey();
|
|
39
|
+
if (!apiKey) {
|
|
40
|
+
throw new Error('Gemini API key is required for semantic code embeddings.');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const client = new GoogleGenAI({ apiKey });
|
|
44
|
+
const response = await client.models.embedContent({
|
|
45
|
+
model,
|
|
46
|
+
contents: text
|
|
47
|
+
});
|
|
48
|
+
return response.embeddings[0].values;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function hashFile(filePath) {
|
|
52
|
+
return crypto.createHash('sha1').update(fs.readFileSync(filePath)).digest('hex');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function cosineSimilarity(vecA, vecB) {
|
|
56
|
+
let dot = 0;
|
|
57
|
+
let normA = 0;
|
|
58
|
+
let normB = 0;
|
|
59
|
+
const length = Math.min(vecA.length, vecB.length);
|
|
60
|
+
|
|
61
|
+
for (let index = 0; index < length; index++) {
|
|
62
|
+
const a = vecA[index];
|
|
63
|
+
const b = vecB[index];
|
|
64
|
+
dot += a * b;
|
|
65
|
+
normA += a * a;
|
|
66
|
+
normB += b * b;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (normA === 0 || normB === 0) return 0;
|
|
70
|
+
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function languageForFile(file) {
|
|
74
|
+
const ext = path.extname(file).toLowerCase();
|
|
75
|
+
if (ext === '.py') return 'Python';
|
|
76
|
+
if (ext === '.rs') return 'Rust';
|
|
77
|
+
if (ext === '.ts' || ext === '.tsx') return 'TypeScript';
|
|
78
|
+
return 'JavaScript';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function chunkLines(lines, maxChars = DEFAULT_MAX_CHARS, overlapLines = DEFAULT_OVERLAP_LINES) {
|
|
82
|
+
const chunks = [];
|
|
83
|
+
let start = 0;
|
|
84
|
+
|
|
85
|
+
while (start < lines.length) {
|
|
86
|
+
let end = start;
|
|
87
|
+
let charCount = 0;
|
|
88
|
+
|
|
89
|
+
while (end < lines.length && (charCount + lines[end].length + 1 <= maxChars || end === start)) {
|
|
90
|
+
charCount += lines[end].length + 1;
|
|
91
|
+
end++;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
chunks.push({
|
|
95
|
+
startLine: start + 1,
|
|
96
|
+
endLine: end,
|
|
97
|
+
content: lines.slice(start, end).join('\n')
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (end >= lines.length) break;
|
|
101
|
+
start = Math.max(end - overlapLines, start + 1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return chunks;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function createCodeChunks(targetPath = process.cwd(), options = {}) {
|
|
108
|
+
const root = path.resolve(targetPath);
|
|
109
|
+
const stat = fs.statSync(root);
|
|
110
|
+
if (!stat.isDirectory()) {
|
|
111
|
+
throw new Error(`Semantic code search path is not a directory: ${root}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const files = symbolHelpers.walkSourceFiles(root, options);
|
|
115
|
+
const maxFileBytes = options.maxFileBytes || DEFAULT_MAX_FILE_BYTES;
|
|
116
|
+
const chunks = [];
|
|
117
|
+
const fileHashes = {};
|
|
118
|
+
|
|
119
|
+
for (const file of files) {
|
|
120
|
+
const fullPath = path.join(root, file);
|
|
121
|
+
let fileStat;
|
|
122
|
+
try {
|
|
123
|
+
fileStat = fs.statSync(fullPath);
|
|
124
|
+
} catch (_) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (!fileStat.isFile() || fileStat.size > maxFileBytes) continue;
|
|
128
|
+
|
|
129
|
+
let content = '';
|
|
130
|
+
try {
|
|
131
|
+
content = fs.readFileSync(fullPath, 'utf8');
|
|
132
|
+
} catch (_) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (!content.trim()) continue;
|
|
136
|
+
|
|
137
|
+
const language = languageForFile(file);
|
|
138
|
+
const symbols = symbolHelpers.indexFileSymbols(root, file).map(symbol => symbol.name);
|
|
139
|
+
fileHashes[file] = hashFile(fullPath);
|
|
140
|
+
|
|
141
|
+
for (const chunk of chunkLines(content.split('\n'), options.maxChars || DEFAULT_MAX_CHARS, options.overlapLines || DEFAULT_OVERLAP_LINES)) {
|
|
142
|
+
const header = [
|
|
143
|
+
`File: ${file}`,
|
|
144
|
+
`Language: ${language}`,
|
|
145
|
+
symbols.length > 0 ? `Symbols: ${symbols.slice(0, 20).join(', ')}` : ''
|
|
146
|
+
].filter(Boolean).join('\n');
|
|
147
|
+
|
|
148
|
+
chunks.push({
|
|
149
|
+
id: `${file}:${chunk.startLine}-${chunk.endLine}`,
|
|
150
|
+
file,
|
|
151
|
+
language,
|
|
152
|
+
startLine: chunk.startLine,
|
|
153
|
+
endLine: chunk.endLine,
|
|
154
|
+
symbols,
|
|
155
|
+
text: `${header}\n\n${chunk.content}`
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { root, files, fileHashes, chunks };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function writeJson(filePath, data) {
|
|
164
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
165
|
+
const tmpPath = `${filePath}.tmp`;
|
|
166
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2), 'utf8');
|
|
167
|
+
fs.renameSync(tmpPath, filePath);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function indexSemanticCode(targetPath = process.cwd(), options = {}) {
|
|
171
|
+
const root = path.resolve(targetPath);
|
|
172
|
+
const embedText = options.embedText || defaultEmbedText;
|
|
173
|
+
const model = options.model || DEFAULT_EMBEDDING_MODEL;
|
|
174
|
+
const storePath = options.storePath || getWorkspaceStorePath(root, { create: true });
|
|
175
|
+
const prepared = createCodeChunks(root, options);
|
|
176
|
+
const indexedChunks = [];
|
|
177
|
+
|
|
178
|
+
for (let index = 0; index < prepared.chunks.length; index++) {
|
|
179
|
+
const chunk = prepared.chunks[index];
|
|
180
|
+
if (typeof options.onProgress === 'function') {
|
|
181
|
+
options.onProgress({ current: index + 1, total: prepared.chunks.length, file: chunk.file });
|
|
182
|
+
}
|
|
183
|
+
const embedding = await embedText(chunk.text, model);
|
|
184
|
+
indexedChunks.push({ ...chunk, embedding });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const payload = {
|
|
188
|
+
version: 1,
|
|
189
|
+
root: prepared.root,
|
|
190
|
+
model,
|
|
191
|
+
indexedAt: new Date().toISOString(),
|
|
192
|
+
fileCount: prepared.files.length,
|
|
193
|
+
chunkCount: indexedChunks.length,
|
|
194
|
+
fileHashes: prepared.fileHashes,
|
|
195
|
+
chunks: indexedChunks
|
|
196
|
+
};
|
|
197
|
+
writeJson(storePath, payload);
|
|
198
|
+
|
|
199
|
+
return { ...payload, storePath };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function loadSemanticCodeIndex(targetPath = process.cwd(), options = {}) {
|
|
203
|
+
const root = path.resolve(targetPath);
|
|
204
|
+
const storePath = options.storePath || getWorkspaceStorePath(root);
|
|
205
|
+
if (!fs.existsSync(storePath)) {
|
|
206
|
+
throw new Error(`Semantic code index not found. Run "mint semantic-code index ${root}" first.`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const payload = JSON.parse(fs.readFileSync(storePath, 'utf8'));
|
|
210
|
+
if (path.resolve(payload.root) !== root) {
|
|
211
|
+
throw new Error(`Semantic code index belongs to a different workspace: ${payload.root}`);
|
|
212
|
+
}
|
|
213
|
+
return { ...payload, storePath };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function searchSemanticCode(query, targetPath = process.cwd(), options = {}) {
|
|
217
|
+
const trimmedQuery = String(query || '').trim();
|
|
218
|
+
if (!trimmedQuery) {
|
|
219
|
+
throw new Error('Semantic code search query is required.');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const index = loadSemanticCodeIndex(targetPath, options);
|
|
223
|
+
const embedText = options.embedText || defaultEmbedText;
|
|
224
|
+
const topK = options.topK || 5;
|
|
225
|
+
const queryEmbedding = await embedText(trimmedQuery, index.model || DEFAULT_EMBEDDING_MODEL);
|
|
226
|
+
|
|
227
|
+
const results = index.chunks
|
|
228
|
+
.filter(chunk => Array.isArray(chunk.embedding))
|
|
229
|
+
.map(chunk => ({
|
|
230
|
+
file: chunk.file,
|
|
231
|
+
language: chunk.language,
|
|
232
|
+
startLine: chunk.startLine,
|
|
233
|
+
endLine: chunk.endLine,
|
|
234
|
+
symbols: chunk.symbols || [],
|
|
235
|
+
score: cosineSimilarity(queryEmbedding, chunk.embedding),
|
|
236
|
+
text: chunk.text
|
|
237
|
+
}))
|
|
238
|
+
.sort((a, b) => b.score - a.score)
|
|
239
|
+
.slice(0, topK);
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
query: trimmedQuery,
|
|
243
|
+
root: index.root,
|
|
244
|
+
indexedAt: index.indexedAt,
|
|
245
|
+
model: index.model,
|
|
246
|
+
resultCount: results.length,
|
|
247
|
+
results
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function formatSemanticCodeIndex(index) {
|
|
252
|
+
return [
|
|
253
|
+
'# Semantic Code Index',
|
|
254
|
+
'',
|
|
255
|
+
`Root: ${index.root}`,
|
|
256
|
+
`Source files scanned: ${index.fileCount}`,
|
|
257
|
+
`Chunks embedded: ${index.chunkCount}`,
|
|
258
|
+
`Embedding model: ${index.model}`,
|
|
259
|
+
`Index file: ${index.storePath}`
|
|
260
|
+
].join('\n');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function firstCodeLine(text) {
|
|
264
|
+
return String(text || '')
|
|
265
|
+
.split('\n')
|
|
266
|
+
.map(line => line.trim())
|
|
267
|
+
.find(line => line && !line.startsWith('File:') && !line.startsWith('Language:') && !line.startsWith('Symbols:')) || '';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function formatSemanticCodeSearch(results) {
|
|
271
|
+
const lines = [
|
|
272
|
+
'# Semantic Code Search',
|
|
273
|
+
'',
|
|
274
|
+
`Query: ${results.query}`,
|
|
275
|
+
`Root: ${results.root}`,
|
|
276
|
+
`Indexed at: ${results.indexedAt}`,
|
|
277
|
+
''
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
if (!results.results.length) {
|
|
281
|
+
lines.push('No matches found.');
|
|
282
|
+
return lines.join('\n');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
results.results.forEach((result, index) => {
|
|
286
|
+
lines.push(`${index + 1}. ${result.file}:${result.startLine}-${result.endLine} (${result.score.toFixed(3)})`);
|
|
287
|
+
if (result.symbols.length > 0) {
|
|
288
|
+
lines.push(` Symbols: ${result.symbols.slice(0, 8).join(', ')}`);
|
|
289
|
+
}
|
|
290
|
+
const preview = firstCodeLine(result.text);
|
|
291
|
+
if (preview) {
|
|
292
|
+
lines.push(` ${preview}`);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
return lines.join('\n');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
module.exports = {
|
|
300
|
+
createCodeChunks,
|
|
301
|
+
indexSemanticCode,
|
|
302
|
+
loadSemanticCodeIndex,
|
|
303
|
+
searchSemanticCode,
|
|
304
|
+
formatSemanticCodeIndex,
|
|
305
|
+
formatSemanticCodeSearch,
|
|
306
|
+
_helpers: {
|
|
307
|
+
chunkLines,
|
|
308
|
+
cosineSimilarity,
|
|
309
|
+
getWorkspaceStorePath,
|
|
310
|
+
defaultEmbedText
|
|
311
|
+
}
|
|
312
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const memoryStore = require('../AI_Brain/memory_store');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Reads a local .md or .txt file and stores it as a persistent Mint skill.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} filePath Path relative to process.cwd() or absolute.
|
|
11
|
+
* @returns {object} The stored skill record from memoryStore.
|
|
12
|
+
* @throws {Error} If the file doesn't exist, isn't a file, has the wrong extension,
|
|
13
|
+
* or exceeds the size limit.
|
|
14
|
+
*/
|
|
15
|
+
function learnSkillFile(filePath) {
|
|
16
|
+
const targetPath = path.resolve(process.cwd(), filePath);
|
|
17
|
+
|
|
18
|
+
if (!fs.existsSync(targetPath)) {
|
|
19
|
+
throw new Error(`File not found: ${targetPath}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const stat = fs.statSync(targetPath);
|
|
23
|
+
if (!stat.isFile()) {
|
|
24
|
+
throw new Error(`Path is not a file: ${targetPath}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const ext = path.extname(targetPath).toLowerCase();
|
|
28
|
+
if (ext !== '.md' && ext !== '.txt') {
|
|
29
|
+
throw new Error('Mint learn currently supports .md and .txt files only.');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const maxBytes = 256 * 1024;
|
|
33
|
+
if (stat.size > maxBytes) {
|
|
34
|
+
throw new Error(`File is too large (${stat.size} bytes). Limit is ${maxBytes} bytes.`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const content = fs.readFileSync(targetPath, 'utf8');
|
|
38
|
+
return memoryStore.addLearnedSkill(path.basename(targetPath), targetPath, content);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { learnSkillFile };
|