@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.
Files changed (33) hide show
  1. package/README.md +8 -0
  2. package/mint-cli.js +148 -921
  3. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json +31 -1
  4. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json +6 -1
  5. package/package.json +18 -20
  6. package/src/AI_Brain/proactive_engine.js +12 -2
  7. package/src/Automation_Layer/browser_automation.js +26 -24
  8. package/src/CLI/approval_handler.js +42 -0
  9. package/src/CLI/chat_ui.js +192 -7
  10. package/src/CLI/cli_colors.js +32 -0
  11. package/src/CLI/cli_formatters.js +89 -0
  12. package/src/CLI/code_agent.js +166 -57
  13. package/src/CLI/intent_detectors.js +181 -0
  14. package/src/CLI/interactive_chat.js +479 -0
  15. package/src/CLI/list_features.js +3 -0
  16. package/src/CLI/repo_summarizer.js +282 -0
  17. package/src/CLI/semantic_code_search.js +312 -0
  18. package/src/CLI/skill_manager.js +41 -0
  19. package/src/CLI/slash_command_handler.js +418 -0
  20. package/src/CLI/symbol_indexer.js +231 -0
  21. package/src/Channels/discord_bridge.js +11 -13
  22. package/src/Channels/line_bridge.js +10 -10
  23. package/src/Channels/slack_bridge.js +7 -12
  24. package/src/Channels/telegram_bridge.js +6 -14
  25. package/src/Channels/whatsapp_bridge.js +11 -9
  26. package/src/System/chat_history_manager.js +20 -12
  27. package/src/System/optional_require.js +23 -0
  28. package/src/UI/live2d_manager.js +211 -13
  29. package/src/UI/renderer.js +163 -3
  30. package/src/UI/settings.css +655 -420
  31. package/src/UI/settings.html +478 -432
  32. package/src/UI/settings.js +10 -8
  33. package/src/UI/styles.css +89 -25
@@ -0,0 +1,418 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const { colors, exitWithGoodbye } = require('./cli_colors');
7
+ const { formatMemoryInteractions } = require('./cli_formatters');
8
+ const { learnSkillFile } = require('./skill_manager');
9
+ const { loadImageAsDataUri, loadClipboardImageAsDataUri } = require('./image_input');
10
+ const { runChatRoutedTask } = require('./chat_router');
11
+ const { getChatTranscript, resetChat } = require('../AI_Brain/Gemini_API');
12
+ const memoryStore = require('../AI_Brain/memory_store');
13
+ const agentOrchestrator = require('../AI_Brain/agent_orchestrator');
14
+ const { readConfig, writeConfig } = require('../System/config_manager');
15
+ const pkg = require('../../package.json');
16
+
17
+ /**
18
+ * Handles all slash commands entered inside the interactive TUI.
19
+ *
20
+ * @param {string} input Full slash command string (e.g. "/memory list")
21
+ * @param {Function} appendMessage
22
+ * @param {Function} updateStatusModel
23
+ * @param {Function} copyLastResponse
24
+ * @param {Function} setThinking
25
+ * @param {Function} requestApproval
26
+ * @param {Function} setMode
27
+ * @param {Function} appendCodeStep
28
+ * @param {Function} updateWorkspace
29
+ * @param {object} helpers Extra helpers injected from interactive_chat
30
+ * @returns {Promise<object|undefined>} May return { lastResponseText } for some commands
31
+ */
32
+ async function handleSlashCommandUI(
33
+ input,
34
+ appendMessage,
35
+ updateStatusModel,
36
+ copyLastResponse,
37
+ setThinking,
38
+ requestApproval,
39
+ setMode,
40
+ appendCodeStep,
41
+ updateWorkspace,
42
+ helpers = {}
43
+ ) {
44
+ const parts = input.split(' ');
45
+ const command = parts[0].toLowerCase();
46
+ const args = parts.slice(1);
47
+
48
+ switch (command) {
49
+ // ------------------------------------------------------------------ /help
50
+ case '/help':
51
+ case '/?':
52
+ appendMessage('system', [
53
+ 'Mint Slash Commands:',
54
+ ' /image <path> [prompt] — Attach an image from your computer',
55
+ ' /paste [prompt] — Attach an image from your clipboard',
56
+ ' /fast [on|off] — Hide or show thinking/progress output',
57
+ ' /summarize [path] [--json] — Summarize repository structure',
58
+ ' /symbols [path] [--json] [--limit n] — Build a source symbol index',
59
+ ' /semantic-code index|search <query> — Embed and search code semantically',
60
+ ' /learn <path> — Remember a .md/.txt file as a Mint skill',
61
+ ' /code <task> — Force workspace Code Mode',
62
+ ' /cd <path> — Change current working directory',
63
+ ' /models [name] — List or switch Gemini models',
64
+ ' /memory [cmd] — Manage long-term memory',
65
+ ' /config — Show current configuration',
66
+ ' /copy — Copy last response to clipboard',
67
+ ' /clear — Clear conversation history',
68
+ ' /reset — Reset conversation history',
69
+ ' /exit — Exit Mint'
70
+ ].join('\n'));
71
+ break;
72
+
73
+ // ------------------------------------------------------------------ /fast
74
+ case '/fast': {
75
+ if (!helpers.toggleFastMode || !helpers.setFastMode || !helpers.getFastMode) {
76
+ appendMessage('error', 'Fast mode is not available in this UI.');
77
+ break;
78
+ }
79
+ const option = (args[0] || '').toLowerCase();
80
+ let enabled;
81
+ if (option === 'on' || option === 'true' || option === '1') enabled = helpers.setFastMode(true);
82
+ else if (option === 'off' || option === 'false' || option === '0') enabled = helpers.setFastMode(false);
83
+ else if (option === 'status') enabled = helpers.getFastMode();
84
+ else enabled = helpers.toggleFastMode();
85
+
86
+ appendMessage('system', `Fast mode: ${enabled ? 'ON' : 'OFF'}`);
87
+ break;
88
+ }
89
+
90
+ // ------------------------------------------------------------------ /summarize
91
+ case '/summarize':
92
+ case '/summary': {
93
+ if (typeof helpers.sendRepoSummaryMessage !== 'function') {
94
+ appendMessage('error', 'Repository summary is not available in this UI.');
95
+ break;
96
+ }
97
+ const responseText = await helpers.sendRepoSummaryMessage({
98
+ rawArgs: input.slice(command.length).trim(),
99
+ appendMessage,
100
+ streamMessage: helpers.streamMessage,
101
+ setThinking
102
+ });
103
+ return { lastResponseText: responseText };
104
+ }
105
+
106
+ // ------------------------------------------------------------------ /symbols
107
+ case '/symbols':
108
+ case '/symbol-index': {
109
+ if (typeof helpers.sendSymbolIndexMessage !== 'function') {
110
+ appendMessage('error', 'Symbol index is not available in this UI.');
111
+ break;
112
+ }
113
+ const responseText = await helpers.sendSymbolIndexMessage({
114
+ rawArgs: input.slice(command.length).trim(),
115
+ appendMessage,
116
+ streamMessage: helpers.streamMessage,
117
+ setThinking
118
+ });
119
+ return { lastResponseText: responseText };
120
+ }
121
+
122
+ // ------------------------------------------------------------------ /semantic-code
123
+ case '/semantic-code':
124
+ case '/semantic': {
125
+ if (typeof helpers.sendSemanticCodeMessage !== 'function') {
126
+ appendMessage('error', 'Semantic code search is not available in this UI.');
127
+ break;
128
+ }
129
+ const responseText = await helpers.sendSemanticCodeMessage({
130
+ rawArgs: input.slice(command.length).trim(),
131
+ appendMessage,
132
+ streamMessage: helpers.streamMessage,
133
+ setThinking,
134
+ appendCodeStep
135
+ });
136
+ return { lastResponseText: responseText };
137
+ }
138
+
139
+ // ------------------------------------------------------------------ /learn
140
+ case '/learn': {
141
+ const filePath = input.slice(command.length).trim();
142
+ if (!filePath) {
143
+ appendMessage('system', 'Usage: /learn <path-to-skill.md>');
144
+ break;
145
+ }
146
+ try {
147
+ const learned = learnSkillFile(filePath);
148
+ appendMessage('system', [
149
+ `✓ Learned skill: ${learned.name}`,
150
+ `Path: ${learned.source_path}`,
151
+ learned.stored_length < learned.content_length
152
+ ? `Stored first ${learned.stored_length} of ${learned.content_length} characters.`
153
+ : `Stored ${learned.stored_length} characters.`
154
+ ].join('\n'));
155
+ } catch (err) {
156
+ appendMessage('error', err && err.message ? err.message : String(err || 'Unknown error'));
157
+ }
158
+ break;
159
+ }
160
+
161
+ // ------------------------------------------------------------------ /image
162
+ case '/image': {
163
+ if (args.length === 0) {
164
+ appendMessage('system', 'Usage: /image <path> [prompt]');
165
+ break;
166
+ }
167
+ const imagePath = args[0];
168
+ const prompt = args.slice(1).join(' ').trim();
169
+ try {
170
+ const image = loadImageAsDataUri(imagePath);
171
+ if (helpers.attachImage) {
172
+ helpers.attachImage({ label: image.path, image });
173
+ if (prompt && helpers.setInputText) helpers.setInputText(prompt);
174
+ appendMessage('system', 'Attached image. Press Enter to send.');
175
+ } else {
176
+ appendMessage('error', 'Image attachment is not available in this UI.');
177
+ }
178
+ } catch (err) {
179
+ appendMessage('error', err && err.message ? err.message : String(err || 'Unknown error'));
180
+ }
181
+ break;
182
+ }
183
+
184
+ // ------------------------------------------------------------------ /paste
185
+ case '/paste': {
186
+ try {
187
+ const image = loadClipboardImageAsDataUri();
188
+ if (helpers.attachImage) {
189
+ helpers.attachImage({ label: image.path, image });
190
+ const prompt = args.join(' ').trim();
191
+ if (prompt && helpers.setInputText) helpers.setInputText(prompt);
192
+ appendMessage('system', 'Attached clipboard image. Press Enter to send.');
193
+ } else {
194
+ appendMessage('error', 'Image attachment is not available in this UI.');
195
+ }
196
+ } catch (err) {
197
+ const msg = helpers.formatErrorMessage
198
+ ? helpers.formatErrorMessage(err)
199
+ : (err && err.message ? err.message : String(err || 'Unknown error'));
200
+ appendMessage('error', msg);
201
+ }
202
+ break;
203
+ }
204
+
205
+ // ------------------------------------------------------------------ /memory
206
+ case '/memory': {
207
+ const subCommand = (args[0] || 'list').toLowerCase();
208
+ const query = args.slice(1).join(' ').trim();
209
+
210
+ if (subCommand === 'help') {
211
+ appendMessage('system', [
212
+ 'Memory Commands:',
213
+ ' /memory list [n] — Show recent remembered interactions',
214
+ ' /memory search <query> — Search remembered interactions',
215
+ ' /memory skills — Show learned skill files',
216
+ ' /memory skills delete <id|path|name> — Delete a learned skill',
217
+ ' /memory profile — Show remembered profile fields',
218
+ ' /memory context [q] — Show context Mint injects into prompts',
219
+ ' /memory delete <id> — Delete one remembered interaction',
220
+ ' /memory export [path] — Export memory snapshot as JSON',
221
+ ' /memory clear — Clear episodic interaction memories'
222
+ ].join('\n'));
223
+ break;
224
+ }
225
+
226
+ if (subCommand === 'profile') {
227
+ const profile = memoryStore.getAllProfile();
228
+ appendMessage('system', Object.keys(profile).length
229
+ ? JSON.stringify(profile, null, 2)
230
+ : 'No profile memory stored yet.');
231
+ break;
232
+ }
233
+
234
+ if (subCommand === 'skills') {
235
+ if ((args[1] || '').toLowerCase() === 'delete') {
236
+ const identifier = args.slice(2).join(' ').trim();
237
+ if (!identifier) {
238
+ appendMessage('system', 'Usage: /memory skills delete <id|path|name>');
239
+ break;
240
+ }
241
+ const deleted = memoryStore.deleteLearnedSkill(identifier);
242
+ appendMessage('system', deleted > 0
243
+ ? `Deleted learned skill: ${identifier}`
244
+ : `Learned skill not found: ${identifier}`);
245
+ break;
246
+ }
247
+ const skills = memoryStore.getLearnedSkills(20);
248
+ appendMessage('system', skills.length
249
+ ? ['Learned skills:', ...skills.map(s => `#${s.id} ${s.name}\n ${s.source_path}`)].join('\n')
250
+ : 'No learned skills stored yet.');
251
+ break;
252
+ }
253
+
254
+ if (subCommand === 'context') {
255
+ const ctx = memoryStore.getUserContext(query);
256
+ appendMessage('system', ctx || 'No memory context stored yet.');
257
+ break;
258
+ }
259
+
260
+ if (subCommand === 'search') {
261
+ if (!query) { appendMessage('system', 'Usage: /memory search <query>'); break; }
262
+ const results = memoryStore.searchInteractions(query, 10);
263
+ appendMessage('system', formatMemoryInteractions(results, `Search results for "${query}"`));
264
+ break;
265
+ }
266
+
267
+ if (subCommand === 'export') {
268
+ const exportPath = query
269
+ ? path.resolve(process.cwd(), query)
270
+ : path.join(process.cwd(), `mint-memory-export-${Date.now()}.json`);
271
+ fs.writeFileSync(exportPath, JSON.stringify(memoryStore.exportMemorySnapshot(), null, 2), 'utf8');
272
+ appendMessage('system', `Memory exported to: ${exportPath}`);
273
+ break;
274
+ }
275
+
276
+ if (subCommand === 'delete') {
277
+ const id = Number.parseInt(args[1] || '', 10);
278
+ if (!Number.isFinite(id)) { appendMessage('system', 'Usage: /memory delete <id>'); break; }
279
+ const deleted = memoryStore.deleteInteractionMemory(id);
280
+ appendMessage('system', deleted ? `Deleted memory #${id}.` : `Memory #${id} was not found.`);
281
+ break;
282
+ }
283
+
284
+ if (subCommand === 'clear') {
285
+ memoryStore.clearInteractionMemories();
286
+ appendMessage('system', 'Cleared episodic interaction memories. Profile memory is unchanged.');
287
+ break;
288
+ }
289
+
290
+ // Default: list recent
291
+ const limit = Number.parseInt(args[0] || '10', 10);
292
+ const interactions = memoryStore.getRecentInteractions(Number.isFinite(limit) ? limit : 10);
293
+ appendMessage('system', formatMemoryInteractions(interactions, 'Recent remembered interactions'));
294
+ break;
295
+ }
296
+
297
+ // ------------------------------------------------------------------ /cd
298
+ case '/cd':
299
+ if (args.length === 0) {
300
+ appendMessage('system', `Current Directory: ${process.cwd()}`);
301
+ break;
302
+ }
303
+ try {
304
+ const newPath = path.resolve(process.cwd(), args[0]);
305
+ if (fs.existsSync(newPath) && fs.lstatSync(newPath).isDirectory()) {
306
+ process.chdir(newPath);
307
+ if (updateWorkspace) updateWorkspace(newPath);
308
+ appendMessage('system', `✓ Directory changed to: ${newPath}`);
309
+ } else {
310
+ appendMessage('error', `Directory not found: ${newPath}`);
311
+ }
312
+ } catch (err) {
313
+ appendMessage('error', `Error: ${err.message}`);
314
+ }
315
+ break;
316
+
317
+ // ------------------------------------------------------------------ /models
318
+ case '/model':
319
+ case '/models': {
320
+ const config = readConfig();
321
+ if (args.length === 0) {
322
+ appendMessage('system', [
323
+ `Current Provider: ${config.aiProvider}`,
324
+ `Current Gemini Model: ${config.geminiModel}`,
325
+ 'Available Providers/Presets:',
326
+ ' - gemini-2.5-flash (Default Gemini)',
327
+ ' - ollama (Local provider)',
328
+ ' - anthropic (Claude)',
329
+ ' - openai (GPT)',
330
+ ' - huggingface (Inference API)',
331
+ ' - local (LM Studio / OpenAI Compatible)',
332
+ 'Usage: /models <name> to switch'
333
+ ].join('\n'));
334
+ } else {
335
+ const newModel = args[0];
336
+ let newProvider = 'gemini';
337
+
338
+ if (newModel === 'ollama') newProvider = 'ollama';
339
+ else if (newModel === 'anthropic') newProvider = 'anthropic';
340
+ else if (newModel === 'openai') newProvider = 'openai';
341
+ else if (newModel === 'huggingface') newProvider = 'huggingface';
342
+ else if (newModel === 'local' || newModel === 'local_openai') newProvider = 'local_openai';
343
+ else if (newModel.startsWith('gpt-')) { newProvider = 'openai'; config.openaiModel = newModel; }
344
+ else if (newModel.startsWith('claude-')) { newProvider = 'anthropic'; config.anthropicModel = newModel; }
345
+ else { newProvider = 'gemini'; config.geminiModel = newModel; }
346
+
347
+ config.aiProvider = newProvider;
348
+ writeConfig(config);
349
+ appendMessage('system', `✅ Switched to: ${newProvider} ${newProvider === 'gemini' ? `(${newModel})` : ''}`);
350
+ if (updateStatusModel) updateStatusModel(newProvider === 'gemini' ? newModel : newProvider);
351
+ }
352
+ break;
353
+ }
354
+
355
+ // ------------------------------------------------------------------ /code
356
+ case '/code':
357
+ if (args.length === 0) {
358
+ appendMessage('system', 'Usage: /code <task>');
359
+ break;
360
+ }
361
+ await runChatRoutedTask(`/code ${args.join(' ')}`, {
362
+ appendMessage,
363
+ setThinking,
364
+ requestApproval,
365
+ appendCodeStep,
366
+ setMode,
367
+ streamAssistantSentences: helpers.streamAssistantSentences,
368
+ streamMessage: helpers.streamMessage,
369
+ askUser: () => Promise.resolve(''),
370
+ history: await getChatTranscript()
371
+ });
372
+ break;
373
+
374
+ // ------------------------------------------------------------------ /config
375
+ case '/config': {
376
+ const currentCfg = readConfig();
377
+ appendMessage('system', [
378
+ 'Current Configuration:',
379
+ ` Version : v${pkg.version}`,
380
+ ` Provider : ${currentCfg.aiProvider}`,
381
+ ` Model : ${currentCfg.geminiModel}`,
382
+ ` Ollama : ${currentCfg.ollamaModel}`,
383
+ ` Voice : ${currentCfg.enableVoiceReply ? 'ON' : 'OFF'}`,
384
+ ` Language : ${currentCfg.language}`,
385
+ ` API Key : ${currentCfg.apiKey ? 'SET (****)' : 'NOT SET'}`
386
+ ].join('\n'));
387
+ break;
388
+ }
389
+
390
+ // ------------------------------------------------------------------ /copy
391
+ case '/copy':
392
+ if (copyLastResponse && copyLastResponse()) {
393
+ appendMessage('system', '✓ Last response copied to clipboard.');
394
+ } else {
395
+ appendMessage('system', '✖ Nothing to copy, or xclip/xsel not installed.');
396
+ }
397
+ break;
398
+
399
+ // ------------------------------------------------------------------ /clear /reset
400
+ case '/clear':
401
+ case '/reset':
402
+ resetChat();
403
+ appendMessage('system', 'Conversation history cleared.');
404
+ break;
405
+
406
+ // ------------------------------------------------------------------ /exit
407
+ case '/exit':
408
+ case '/quit':
409
+ exitWithGoodbye(0);
410
+ break;
411
+
412
+ // ------------------------------------------------------------------ default
413
+ default:
414
+ appendMessage('system', `Unknown command: ${command}. Type /help for options.`);
415
+ }
416
+ }
417
+
418
+ module.exports = { handleSlashCommandUI };
@@ -0,0 +1,231 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const IGNORED_DIRS = new Set([
5
+ '.git',
6
+ '.cache',
7
+ '.next',
8
+ '.nuxt',
9
+ 'coverage',
10
+ 'dist',
11
+ 'build',
12
+ 'out',
13
+ 'node_modules'
14
+ ]);
15
+
16
+ const LANGUAGE_BY_EXT = {
17
+ '.cjs': 'JavaScript',
18
+ '.js': 'JavaScript',
19
+ '.jsx': 'JavaScript',
20
+ '.mjs': 'JavaScript',
21
+ '.py': 'Python',
22
+ '.rs': 'Rust',
23
+ '.ts': 'TypeScript',
24
+ '.tsx': 'TypeScript'
25
+ };
26
+
27
+ const SOURCE_EXTENSIONS = new Set(Object.keys(LANGUAGE_BY_EXT));
28
+
29
+ function walkSourceFiles(root, options = {}) {
30
+ const maxFiles = options.maxFiles || 2500;
31
+ const files = [];
32
+
33
+ function visit(dir) {
34
+ let entries = [];
35
+ try {
36
+ entries = fs.readdirSync(dir, { withFileTypes: true });
37
+ } catch (_) {
38
+ return;
39
+ }
40
+
41
+ entries.sort((a, b) => a.name.localeCompare(b.name));
42
+ for (const entry of entries) {
43
+ if (files.length >= maxFiles) return;
44
+ const fullPath = path.join(dir, entry.name);
45
+ const relativePath = path.relative(root, fullPath);
46
+
47
+ if (entry.isDirectory()) {
48
+ if (IGNORED_DIRS.has(entry.name)) continue;
49
+ visit(fullPath);
50
+ continue;
51
+ }
52
+
53
+ if (entry.isFile() && SOURCE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) {
54
+ files.push(relativePath);
55
+ }
56
+ }
57
+ }
58
+
59
+ visit(root);
60
+ return files;
61
+ }
62
+
63
+ function makeSymbol({ name, kind, file, line, column, language, signature }) {
64
+ return {
65
+ name,
66
+ kind,
67
+ file,
68
+ line,
69
+ column,
70
+ language,
71
+ signature: signature.trim()
72
+ };
73
+ }
74
+
75
+ function scanPattern(lines, file, language, pattern, kind, symbols) {
76
+ lines.forEach((lineText, index) => {
77
+ const match = lineText.match(pattern);
78
+ if (!match) return;
79
+
80
+ const name = match.groups?.name || match[1];
81
+ if (!name) return;
82
+
83
+ symbols.push(makeSymbol({
84
+ name,
85
+ kind,
86
+ file,
87
+ line: index + 1,
88
+ column: lineText.indexOf(name) + 1,
89
+ language,
90
+ signature: lineText.trim()
91
+ }));
92
+ });
93
+ }
94
+
95
+ function indexJavaScriptLike(content, file, language) {
96
+ const lines = content.split('\n');
97
+ const symbols = [];
98
+
99
+ scanPattern(lines, file, language, /^\s*(?:export\s+)?(?:async\s+)?function\*?\s+(?<name>[A-Za-z_$][\w$]*)\s*\(/, 'function', symbols);
100
+ scanPattern(lines, file, language, /^\s*(?:export\s+)?class\s+(?<name>[A-Za-z_$][\w$]*)\b/, 'class', symbols);
101
+ scanPattern(lines, file, language, /^\s*(?:export\s+)?(?:const|let|var)\s+(?<name>[A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>/, 'function', symbols);
102
+ scanPattern(lines, file, language, /^\s*(?:export\s+)?(?:const|let|var)\s+(?<name>[A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?function\b/, 'function', symbols);
103
+ scanPattern(lines, file, language, /^\s*(?:export\s+)?interface\s+(?<name>[A-Za-z_$][\w$]*)\b/, 'interface', symbols);
104
+ scanPattern(lines, file, language, /^\s*(?:export\s+)?type\s+(?<name>[A-Za-z_$][\w$]*)\b/, 'type', symbols);
105
+ scanPattern(lines, file, language, /^\s*(?:export\s+)?enum\s+(?<name>[A-Za-z_$][\w$]*)\b/, 'enum', symbols);
106
+ scanPattern(lines, file, language, /^\s*(?:module\.)?exports\.(?<name>[A-Za-z_$][\w$]*)\s*=/, 'export', symbols);
107
+
108
+ return symbols;
109
+ }
110
+
111
+ function indexPython(content, file) {
112
+ const lines = content.split('\n');
113
+ const symbols = [];
114
+
115
+ scanPattern(lines, file, 'Python', /^\s*def\s+(?<name>[A-Za-z_]\w*)\s*\(/, 'function', symbols);
116
+ scanPattern(lines, file, 'Python', /^\s*async\s+def\s+(?<name>[A-Za-z_]\w*)\s*\(/, 'function', symbols);
117
+ scanPattern(lines, file, 'Python', /^\s*class\s+(?<name>[A-Za-z_]\w*)\b/, 'class', symbols);
118
+
119
+ return symbols;
120
+ }
121
+
122
+ function indexRust(content, file) {
123
+ const lines = content.split('\n');
124
+ const symbols = [];
125
+
126
+ scanPattern(lines, file, 'Rust', /^\s*(?:pub\s+)?(?:async\s+)?fn\s+(?<name>[A-Za-z_]\w*)\s*\(/, 'function', symbols);
127
+ scanPattern(lines, file, 'Rust', /^\s*(?:pub\s+)?struct\s+(?<name>[A-Za-z_]\w*)\b/, 'struct', symbols);
128
+ scanPattern(lines, file, 'Rust', /^\s*(?:pub\s+)?enum\s+(?<name>[A-Za-z_]\w*)\b/, 'enum', symbols);
129
+ scanPattern(lines, file, 'Rust', /^\s*(?:pub\s+)?trait\s+(?<name>[A-Za-z_]\w*)\b/, 'trait', symbols);
130
+ scanPattern(lines, file, 'Rust', /^\s*impl(?:\s+\w+)?\s+for\s+(?<name>[A-Za-z_]\w*)\b/, 'impl', symbols);
131
+
132
+ return symbols;
133
+ }
134
+
135
+ function indexFileSymbols(root, relativePath) {
136
+ const fullPath = path.join(root, relativePath);
137
+ const ext = path.extname(relativePath).toLowerCase();
138
+ const language = LANGUAGE_BY_EXT[ext] || 'Other';
139
+ let content = '';
140
+
141
+ try {
142
+ content = fs.readFileSync(fullPath, 'utf8');
143
+ } catch (_) {
144
+ return [];
145
+ }
146
+
147
+ if (language === 'Python') return indexPython(content, relativePath);
148
+ if (language === 'Rust') return indexRust(content, relativePath);
149
+ return indexJavaScriptLike(content, relativePath, language);
150
+ }
151
+
152
+ function countBy(items, key) {
153
+ const counts = new Map();
154
+ for (const item of items) {
155
+ const value = item[key] || 'unknown';
156
+ counts.set(value, (counts.get(value) || 0) + 1);
157
+ }
158
+ return Array.from(counts.entries())
159
+ .map(([name, count]) => ({ name, count }))
160
+ .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
161
+ }
162
+
163
+ function buildSymbolIndex(targetPath = process.cwd(), options = {}) {
164
+ const root = path.resolve(targetPath);
165
+ const stat = fs.statSync(root);
166
+ if (!stat.isDirectory()) {
167
+ throw new Error(`Symbol index path is not a directory: ${root}`);
168
+ }
169
+
170
+ const files = walkSourceFiles(root, options);
171
+ const symbols = files.flatMap(file => indexFileSymbols(root, file));
172
+
173
+ return {
174
+ root,
175
+ fileCount: files.length,
176
+ indexedFiles: [...new Set(symbols.map(symbol => symbol.file))].length,
177
+ symbolCount: symbols.length,
178
+ kindCounts: countBy(symbols, 'kind'),
179
+ languageCounts: countBy(symbols, 'language'),
180
+ symbols: symbols.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.name.localeCompare(b.name))
181
+ };
182
+ }
183
+
184
+ function formatSymbolIndex(index, options = {}) {
185
+ const limit = Number.isFinite(options.limit) ? options.limit : 80;
186
+ const shown = index.symbols.slice(0, limit);
187
+ const lines = [];
188
+
189
+ lines.push('# Symbol Index');
190
+ lines.push('');
191
+ lines.push(`Root: ${index.root}`);
192
+ lines.push(`Source files scanned: ${index.fileCount}`);
193
+ lines.push(`Files with symbols: ${index.indexedFiles}`);
194
+ lines.push(`Symbols found: ${index.symbolCount}`);
195
+
196
+ lines.push('');
197
+ lines.push('## By Kind');
198
+ lines.push(index.kindCounts.length
199
+ ? index.kindCounts.map(item => `- ${item.name}: ${item.count}`).join('\n')
200
+ : '- (none)');
201
+
202
+ lines.push('');
203
+ lines.push('## By Language');
204
+ lines.push(index.languageCounts.length
205
+ ? index.languageCounts.map(item => `- ${item.name}: ${item.count}`).join('\n')
206
+ : '- (none)');
207
+
208
+ lines.push('');
209
+ lines.push(`## Symbols${index.symbolCount > shown.length ? ` (first ${shown.length})` : ''}`);
210
+ if (shown.length === 0) {
211
+ lines.push('- (none)');
212
+ } else {
213
+ shown.forEach(symbol => {
214
+ lines.push(`- ${symbol.kind} ${symbol.name} (${symbol.file}:${symbol.line})`);
215
+ });
216
+ }
217
+
218
+ return lines.join('\n');
219
+ }
220
+
221
+ module.exports = {
222
+ buildSymbolIndex,
223
+ formatSymbolIndex,
224
+ _helpers: {
225
+ walkSourceFiles,
226
+ indexFileSymbols,
227
+ indexJavaScriptLike,
228
+ indexPython,
229
+ indexRust
230
+ }
231
+ };