dev-mcp-server 0.0.2 → 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/.env.example +23 -55
- package/README.md +609 -219
- package/cli.js +486 -160
- package/package.json +2 -2
- package/src/agents/BaseAgent.js +113 -0
- package/src/agents/dreamer.js +165 -0
- package/src/agents/improver.js +175 -0
- package/src/agents/specialists.js +202 -0
- package/src/agents/taskDecomposer.js +176 -0
- package/src/agents/teamCoordinator.js +153 -0
- package/src/api/routes/agents.js +172 -0
- package/src/api/routes/extras.js +115 -0
- package/src/api/routes/git.js +72 -0
- package/src/api/routes/ingest.js +60 -40
- package/src/api/routes/knowledge.js +59 -41
- package/src/api/routes/memory.js +41 -0
- package/src/api/routes/newRoutes.js +168 -0
- package/src/api/routes/pipelines.js +41 -0
- package/src/api/routes/planner.js +54 -0
- package/src/api/routes/query.js +24 -0
- package/src/api/routes/sessions.js +54 -0
- package/src/api/routes/tasks.js +67 -0
- package/src/api/routes/tools.js +85 -0
- package/src/api/routes/v5routes.js +196 -0
- package/src/api/server.js +133 -5
- package/src/context/compactor.js +151 -0
- package/src/context/contextEngineer.js +181 -0
- package/src/context/contextVisualizer.js +140 -0
- package/src/core/conversationEngine.js +231 -0
- package/src/core/indexer.js +169 -143
- package/src/core/ingester.js +141 -126
- package/src/core/queryEngine.js +286 -236
- package/src/cron/cronScheduler.js +260 -0
- package/src/dashboard/index.html +1181 -0
- package/src/lsp/symbolNavigator.js +220 -0
- package/src/memory/memoryManager.js +186 -0
- package/src/memory/teamMemory.js +111 -0
- package/src/messaging/messageBus.js +177 -0
- package/src/monitor/proactiveMonitor.js +337 -0
- package/src/pipelines/pipelineEngine.js +230 -0
- package/src/planner/plannerEngine.js +202 -0
- package/src/plugins/builtin/stats-plugin.js +29 -0
- package/src/plugins/pluginManager.js +144 -0
- package/src/prompts/promptEngineer.js +289 -0
- package/src/sessions/sessionManager.js +166 -0
- package/src/skills/skillsManager.js +263 -0
- package/src/storage/store.js +127 -105
- package/src/tasks/taskManager.js +151 -0
- package/src/tools/BashTool.js +154 -0
- package/src/tools/FileEditTool.js +280 -0
- package/src/tools/GitTool.js +212 -0
- package/src/tools/GrepTool.js +199 -0
- package/src/tools/registry.js +1380 -0
- package/src/utils/costTracker.js +69 -0
- package/src/utils/fileParser.js +176 -153
- package/src/utils/llmClient.js +355 -206
- package/src/watcher/fileWatcher.js +137 -0
- package/src/worktrees/worktreeManager.js +176 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Provider-agnostic cost tracking. Uses llmClient for price lookup.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const COST_FILE = path.join(process.cwd(), 'data', 'cost-tracker.json');
|
|
10
|
+
|
|
11
|
+
class CostTracker {
|
|
12
|
+
constructor() {
|
|
13
|
+
this._data = this._load();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
_load() {
|
|
17
|
+
try { if (fs.existsSync(COST_FILE)) return JSON.parse(fs.readFileSync(COST_FILE, 'utf-8')); } catch { }
|
|
18
|
+
return { sessions: {}, allTime: { inputTokens: 0, outputTokens: 0, totalCostUsd: 0, calls: 0 } };
|
|
19
|
+
}
|
|
20
|
+
_save() { fs.writeFileSync(COST_FILE, JSON.stringify(this._data, null, 2)); }
|
|
21
|
+
|
|
22
|
+
record({ model, inputTokens, outputTokens, sessionId = 'default', queryType = 'general' }) {
|
|
23
|
+
// Lazy-load llmClient to avoid circular dep at startup
|
|
24
|
+
let costUsd = 0;
|
|
25
|
+
try {
|
|
26
|
+
const llm = require('./llmClient');
|
|
27
|
+
costUsd = llm.costUsd(model, inputTokens, outputTokens);
|
|
28
|
+
} catch { }
|
|
29
|
+
|
|
30
|
+
this._data.allTime.inputTokens += inputTokens;
|
|
31
|
+
this._data.allTime.outputTokens += outputTokens;
|
|
32
|
+
this._data.allTime.totalCostUsd += costUsd;
|
|
33
|
+
this._data.allTime.calls += 1;
|
|
34
|
+
|
|
35
|
+
if (!this._data.sessions[sessionId]) {
|
|
36
|
+
this._data.sessions[sessionId] = { startedAt: new Date().toISOString(), inputTokens: 0, outputTokens: 0, totalCostUsd: 0, calls: 0, byQueryType: {} };
|
|
37
|
+
}
|
|
38
|
+
const sess = this._data.sessions[sessionId];
|
|
39
|
+
sess.inputTokens += inputTokens;
|
|
40
|
+
sess.outputTokens += outputTokens;
|
|
41
|
+
sess.totalCostUsd += costUsd;
|
|
42
|
+
sess.calls += 1;
|
|
43
|
+
sess.lastUsedAt = new Date().toISOString();
|
|
44
|
+
sess.byQueryType[queryType] = (sess.byQueryType[queryType] || 0) + 1;
|
|
45
|
+
|
|
46
|
+
this._save();
|
|
47
|
+
return { costUsd, inputTokens, outputTokens };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getSession(id = 'default') { return this._data.sessions[id] || null; }
|
|
51
|
+
getAllTime() { return this._data.allTime; }
|
|
52
|
+
|
|
53
|
+
getSummary(sessionId = 'default') {
|
|
54
|
+
const sess = this.getSession(sessionId);
|
|
55
|
+
const all = this.getAllTime();
|
|
56
|
+
return {
|
|
57
|
+
session: sess ? { calls: sess.calls, inputTokens: sess.inputTokens, outputTokens: sess.outputTokens, costUsd: parseFloat(sess.totalCostUsd.toFixed(6)), startedAt: sess.startedAt } : null,
|
|
58
|
+
allTime: { calls: all.calls, inputTokens: all.inputTokens, outputTokens: all.outputTokens, costUsd: parseFloat(all.totalCostUsd.toFixed(6)) },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
formatCost(usd) {
|
|
63
|
+
if (usd === 0) return '$0.0000 (local)';
|
|
64
|
+
if (usd < 0.001) return `$${(usd * 1000).toFixed(4)}m`;
|
|
65
|
+
return `$${usd.toFixed(4)}`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = new CostTracker();
|
package/src/utils/fileParser.js
CHANGED
|
@@ -1,183 +1,206 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
|
|
4
|
+
// Supported file types and their "kind" labels
|
|
4
5
|
const FILE_TYPE_MAP = {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
6
|
+
'.js': 'code',
|
|
7
|
+
'.ts': 'code',
|
|
8
|
+
'.jsx': 'code',
|
|
9
|
+
'.tsx': 'code',
|
|
10
|
+
'.mjs': 'code',
|
|
11
|
+
'.cjs': 'code',
|
|
12
|
+
'.py': 'code',
|
|
13
|
+
'.java': 'code',
|
|
14
|
+
'.go': 'code',
|
|
15
|
+
'.rb': 'code',
|
|
16
|
+
'.php': 'code',
|
|
17
|
+
'.cs': 'code',
|
|
18
|
+
'.cpp': 'code',
|
|
19
|
+
'.c': 'code',
|
|
20
|
+
'.rs': 'code',
|
|
21
|
+
'.json': 'config',
|
|
22
|
+
'.yaml': 'config',
|
|
23
|
+
'.yml': 'config',
|
|
24
|
+
'.env': 'config',
|
|
25
|
+
'.toml': 'config',
|
|
26
|
+
'.xml': 'config',
|
|
27
|
+
'.md': 'documentation',
|
|
28
|
+
'.txt': 'documentation',
|
|
29
|
+
'.log': 'log',
|
|
30
|
+
'.sql': 'schema',
|
|
31
|
+
'.graphql': 'schema',
|
|
32
|
+
'.gql': 'schema',
|
|
33
|
+
'.sh': 'script',
|
|
34
|
+
'.bash': 'script',
|
|
34
35
|
};
|
|
35
36
|
|
|
36
37
|
const CHUNK_SIZE = 1500; // characters per chunk
|
|
37
38
|
const CHUNK_OVERLAP = 200; // overlap between chunks
|
|
38
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Parse a file and return structured chunks for indexing
|
|
42
|
+
*/
|
|
39
43
|
function parseFile(filePath) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
44
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
45
|
+
const kind = FILE_TYPE_MAP[ext] || 'unknown';
|
|
46
|
+
const filename = path.basename(filePath);
|
|
47
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
48
|
+
|
|
49
|
+
if (!content.trim()) return [];
|
|
50
|
+
|
|
51
|
+
const chunks = chunkContent(content, filePath);
|
|
52
|
+
|
|
53
|
+
return chunks.map((chunk, idx) => ({
|
|
54
|
+
id: `${filePath}::chunk${idx}`,
|
|
55
|
+
filePath,
|
|
56
|
+
filename,
|
|
57
|
+
ext,
|
|
58
|
+
kind,
|
|
59
|
+
chunkIndex: idx,
|
|
60
|
+
totalChunks: chunks.length,
|
|
61
|
+
content: chunk,
|
|
62
|
+
lines: countLines(chunk),
|
|
63
|
+
ingestedAt: new Date().toISOString(),
|
|
64
|
+
// Extract code-specific metadata
|
|
65
|
+
metadata: extractMetadata(chunk, kind, filename),
|
|
66
|
+
}));
|
|
62
67
|
}
|
|
63
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Split content into overlapping chunks
|
|
71
|
+
*/
|
|
64
72
|
function chunkContent(content, filePath) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
73
|
+
const lines = content.split('\n');
|
|
74
|
+
|
|
75
|
+
// For small files, keep as single chunk
|
|
76
|
+
if (content.length <= CHUNK_SIZE) return [content];
|
|
77
|
+
|
|
78
|
+
const chunks = [];
|
|
79
|
+
let current = [];
|
|
80
|
+
let currentLen = 0;
|
|
81
|
+
|
|
82
|
+
for (const line of lines) {
|
|
83
|
+
current.push(line);
|
|
84
|
+
currentLen += line.length + 1;
|
|
85
|
+
|
|
86
|
+
if (currentLen >= CHUNK_SIZE) {
|
|
87
|
+
chunks.push(current.join('\n'));
|
|
88
|
+
// Keep last N chars for overlap
|
|
89
|
+
const overlapLines = [];
|
|
90
|
+
let overlapLen = 0;
|
|
91
|
+
for (let i = current.length - 1; i >= 0 && overlapLen < CHUNK_OVERLAP; i--) {
|
|
92
|
+
overlapLines.unshift(current[i]);
|
|
93
|
+
overlapLen += current[i].length + 1;
|
|
94
|
+
}
|
|
95
|
+
current = overlapLines;
|
|
96
|
+
currentLen = overlapLen;
|
|
88
97
|
}
|
|
98
|
+
}
|
|
89
99
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
100
|
+
if (current.length > 0) {
|
|
101
|
+
chunks.push(current.join('\n'));
|
|
102
|
+
}
|
|
93
103
|
|
|
94
|
-
|
|
104
|
+
return chunks;
|
|
95
105
|
}
|
|
96
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Extract metadata from file content based on its type
|
|
109
|
+
*/
|
|
97
110
|
function extractMetadata(content, kind, filename) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const classMatches = content.matchAll(/class\s+(\w+)/g);
|
|
115
|
-
for (const m of classMatches) classes.push(m[1]);
|
|
116
|
-
|
|
117
|
-
const importMatches = content.matchAll(/(?:import|require)\s*(?:\{[^}]+\}|[\w*]+)?\s*(?:from)?\s*['"]([^'"]+)['"]/g);
|
|
118
|
-
for (const m of importMatches) imports.push(m[1]);
|
|
119
|
-
|
|
120
|
-
const exportMatches = content.matchAll(/export\s+(?:default\s+)?(?:function|class|const|let|var)?\s*(\w+)/g);
|
|
121
|
-
for (const m of exportMatches) exports.push(m[1]);
|
|
122
|
-
|
|
123
|
-
const errors = [];
|
|
124
|
-
const errorMatches = content.matchAll(/(?:catch|throw|Error|Exception)\s*[(\s]*([A-Z]\w+(?:Error|Exception))/g);
|
|
125
|
-
for (const m of errorMatches) errors.push(m[1]);
|
|
126
|
-
|
|
127
|
-
meta.functions = [...new Set(functions)].slice(0, 20);
|
|
128
|
-
meta.classes = [...new Set(classes)].slice(0, 10);
|
|
129
|
-
meta.imports = [...new Set(imports)].slice(0, 20);
|
|
130
|
-
meta.exports = [...new Set(exports)].slice(0, 10);
|
|
131
|
-
meta.errors = [...new Set(errors)].slice(0, 10);
|
|
132
|
-
|
|
133
|
-
const apiPatterns = [];
|
|
134
|
-
if (/express|router\.(get|post|put|delete|patch)/i.test(content)) apiPatterns.push('REST API');
|
|
135
|
-
if (/graphql|resolver|schema/i.test(content)) apiPatterns.push('GraphQL');
|
|
136
|
-
if (/mongoose|sequelize|prisma|typeorm/i.test(content)) apiPatterns.push('ORM');
|
|
137
|
-
if (/redis|ioredis|bull/i.test(content)) apiPatterns.push('Cache/Queue');
|
|
138
|
-
if (/jwt|passport|bcrypt/i.test(content)) apiPatterns.push('Auth');
|
|
139
|
-
meta.patterns = apiPatterns;
|
|
140
|
-
|
|
141
|
-
} else if (kind === 'log') {
|
|
142
|
-
const errorLines = content
|
|
143
|
-
.split('\n')
|
|
144
|
-
.filter(l => /error|exception|warn|fail|crash/i.test(l))
|
|
145
|
-
.slice(0, 10);
|
|
146
|
-
meta.errors = errorLines;
|
|
147
|
-
|
|
148
|
-
const hasTimestamps = /\d{4}-\d{2}-\d{2}|\d{2}\/\d{2}\/\d{4}/.test(content);
|
|
149
|
-
meta.hasTimestamps = hasTimestamps;
|
|
150
|
-
|
|
151
|
-
} else if (kind === 'schema') {
|
|
152
|
-
const tables = [];
|
|
153
|
-
const tableMatches = content.matchAll(/(?:CREATE\s+TABLE|model\s+|type\s+)["'`]?(\w+)["'`]?/gi);
|
|
154
|
-
for (const m of tableMatches) tables.push(m[1]);
|
|
155
|
-
meta.tables = [...new Set(tables)].slice(0, 20);
|
|
111
|
+
const meta = {};
|
|
112
|
+
|
|
113
|
+
if (kind === 'code') {
|
|
114
|
+
// Extract function/class names
|
|
115
|
+
const functions = [];
|
|
116
|
+
const classes = [];
|
|
117
|
+
const imports = [];
|
|
118
|
+
const exports = [];
|
|
119
|
+
|
|
120
|
+
const fnMatches = content.matchAll(/(?:function\s+(\w+)|const\s+(\w+)\s*=\s*(?:async\s*)?\(|(\w+)\s*\([^)]*\)\s*{)/g);
|
|
121
|
+
for (const m of fnMatches) {
|
|
122
|
+
const name = m[1] || m[2] || m[3];
|
|
123
|
+
if (name && !['if', 'for', 'while', 'switch', 'catch'].includes(name)) {
|
|
124
|
+
functions.push(name);
|
|
125
|
+
}
|
|
156
126
|
}
|
|
157
127
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
128
|
+
const classMatches = content.matchAll(/class\s+(\w+)/g);
|
|
129
|
+
for (const m of classMatches) classes.push(m[1]);
|
|
130
|
+
|
|
131
|
+
const importMatches = content.matchAll(/(?:import|require)\s*(?:\{[^}]+\}|[\w*]+)?\s*(?:from)?\s*['"]([^'"]+)['"]/g);
|
|
132
|
+
for (const m of importMatches) imports.push(m[1]);
|
|
133
|
+
|
|
134
|
+
const exportMatches = content.matchAll(/export\s+(?:default\s+)?(?:function|class|const|let|var)?\s*(\w+)/g);
|
|
135
|
+
for (const m of exportMatches) exports.push(m[1]);
|
|
136
|
+
|
|
137
|
+
// Extract error types mentioned
|
|
138
|
+
const errors = [];
|
|
139
|
+
const errorMatches = content.matchAll(/(?:catch|throw|Error|Exception)\s*[(\s]*([A-Z]\w+(?:Error|Exception))/g);
|
|
140
|
+
for (const m of errorMatches) errors.push(m[1]);
|
|
141
|
+
|
|
142
|
+
meta.functions = [...new Set(functions)].slice(0, 20);
|
|
143
|
+
meta.classes = [...new Set(classes)].slice(0, 10);
|
|
144
|
+
meta.imports = [...new Set(imports)].slice(0, 20);
|
|
145
|
+
meta.exports = [...new Set(exports)].slice(0, 10);
|
|
146
|
+
meta.errors = [...new Set(errors)].slice(0, 10);
|
|
147
|
+
|
|
148
|
+
// Detect API patterns
|
|
149
|
+
const apiPatterns = [];
|
|
150
|
+
if (/express|router\.(get|post|put|delete|patch)/i.test(content)) apiPatterns.push('REST API');
|
|
151
|
+
if (/graphql|resolver|schema/i.test(content)) apiPatterns.push('GraphQL');
|
|
152
|
+
if (/mongoose|sequelize|prisma|typeorm/i.test(content)) apiPatterns.push('ORM');
|
|
153
|
+
if (/redis|ioredis|bull/i.test(content)) apiPatterns.push('Cache/Queue');
|
|
154
|
+
if (/jwt|passport|bcrypt/i.test(content)) apiPatterns.push('Auth');
|
|
155
|
+
meta.patterns = apiPatterns;
|
|
156
|
+
|
|
157
|
+
} else if (kind === 'log') {
|
|
158
|
+
// Extract error/warning lines
|
|
159
|
+
const errorLines = content
|
|
160
|
+
.split('\n')
|
|
161
|
+
.filter(l => /error|exception|warn|fail|crash/i.test(l))
|
|
162
|
+
.slice(0, 10);
|
|
163
|
+
meta.errors = errorLines;
|
|
164
|
+
|
|
165
|
+
// Extract timestamps if present
|
|
166
|
+
const hasTimestamps = /\d{4}-\d{2}-\d{2}|\d{2}\/\d{2}\/\d{4}/.test(content);
|
|
167
|
+
meta.hasTimestamps = hasTimestamps;
|
|
168
|
+
|
|
169
|
+
} else if (kind === 'schema') {
|
|
170
|
+
// Extract table/model names
|
|
171
|
+
const tables = [];
|
|
172
|
+
const tableMatches = content.matchAll(/(?:CREATE\s+TABLE|model\s+|type\s+)["'`]?(\w+)["'`]?/gi);
|
|
173
|
+
for (const m of tableMatches) tables.push(m[1]);
|
|
174
|
+
meta.tables = [...new Set(tables)].slice(0, 20);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Detect if this looks like a bug fix / patch
|
|
178
|
+
meta.isBugFix = /fix|bug|patch|resolve|hotfix|issue/i.test(filename) ||
|
|
179
|
+
/TODO|FIXME|HACK|BUG|XXX/.test(content);
|
|
180
|
+
|
|
181
|
+
return meta;
|
|
162
182
|
}
|
|
163
183
|
|
|
164
184
|
function countLines(text) {
|
|
165
|
-
|
|
185
|
+
return text.split('\n').length;
|
|
166
186
|
}
|
|
167
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Check if a file should be skipped
|
|
190
|
+
*/
|
|
168
191
|
function shouldSkip(filePath) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
192
|
+
const skipPatterns = [
|
|
193
|
+
/node_modules/,
|
|
194
|
+
/\.git\//,
|
|
195
|
+
/dist\//,
|
|
196
|
+
/build\//,
|
|
197
|
+
/coverage\//,
|
|
198
|
+
/\.min\.(js|css)$/,
|
|
199
|
+
/package-lock\.json$/,
|
|
200
|
+
/yarn\.lock$/,
|
|
201
|
+
/\.map$/,
|
|
202
|
+
];
|
|
203
|
+
return skipPatterns.some(p => p.test(filePath));
|
|
181
204
|
}
|
|
182
205
|
|
|
183
206
|
module.exports = { parseFile, shouldSkip, FILE_TYPE_MAP };
|