smart-context-mcp 0.8.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.
@@ -0,0 +1,216 @@
1
+ import path from 'node:path';
2
+ import ts from 'typescript';
3
+ import { joinSections, toUniqueLines, truncateSection } from './shared.js';
4
+
5
+ const scriptKindByExtension = {
6
+ '.js': ts.ScriptKind.JS,
7
+ '.jsx': ts.ScriptKind.JSX,
8
+ '.ts': ts.ScriptKind.TS,
9
+ '.tsx': ts.ScriptKind.TSX,
10
+ '.mjs': ts.ScriptKind.JS,
11
+ '.cjs': ts.ScriptKind.JS,
12
+ };
13
+
14
+ const getNodeName = (node) => {
15
+ if (node.name && ts.isIdentifier(node.name)) {
16
+ return node.name.text;
17
+ }
18
+
19
+ return 'anonymous';
20
+ };
21
+
22
+ const formatImport = (statement) => {
23
+ const moduleName = statement.moduleSpecifier.getText();
24
+ const clause = statement.importClause;
25
+
26
+ if (!clause) {
27
+ return `import ${moduleName}`;
28
+ }
29
+
30
+ const parts = [];
31
+
32
+ if (clause.name) {
33
+ parts.push(clause.name.text);
34
+ }
35
+
36
+ if (clause.namedBindings) {
37
+ if (ts.isNamespaceImport(clause.namedBindings)) {
38
+ parts.push(`* as ${clause.namedBindings.name.text}`);
39
+ } else {
40
+ const names = clause.namedBindings.elements.map((element) => {
41
+ if (element.propertyName) {
42
+ return `${element.propertyName.text} as ${element.name.text}`;
43
+ }
44
+
45
+ return element.name.text;
46
+ });
47
+ parts.push(`{ ${names.join(', ')} }`);
48
+ }
49
+ }
50
+
51
+ return `import ${parts.join(', ')} from ${moduleName}`;
52
+ };
53
+
54
+ const formatDeclarationName = (name) => {
55
+ if (ts.isIdentifier(name)) {
56
+ return name.text;
57
+ }
58
+
59
+ return name.getText();
60
+ };
61
+
62
+ const collectVariableNames = (declarationList) => declarationList.declarations.map((declaration) => formatDeclarationName(declaration.name));
63
+
64
+ const formatTopLevelStatement = (statement, sourceFile) => {
65
+ const exported = statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false;
66
+ const prefix = exported ? 'export ' : '';
67
+
68
+ if (ts.isImportDeclaration(statement)) {
69
+ return formatImport(statement);
70
+ }
71
+
72
+ if (ts.isFunctionDeclaration(statement)) {
73
+ return `${prefix}function ${getNodeName(statement)}()`;
74
+ }
75
+
76
+ if (ts.isClassDeclaration(statement)) {
77
+ return `${prefix}class ${getNodeName(statement)}`;
78
+ }
79
+
80
+ if (ts.isInterfaceDeclaration(statement)) {
81
+ return `${prefix}interface ${statement.name.text}`;
82
+ }
83
+
84
+ if (ts.isTypeAliasDeclaration(statement)) {
85
+ return `${prefix}type ${statement.name.text}`;
86
+ }
87
+
88
+ if (ts.isEnumDeclaration(statement)) {
89
+ return `${prefix}enum ${statement.name.text}`;
90
+ }
91
+
92
+ if (ts.isVariableStatement(statement)) {
93
+ const declarationKind = statement.declarationList.flags & ts.NodeFlags.Const
94
+ ? 'const'
95
+ : statement.declarationList.flags & ts.NodeFlags.Let
96
+ ? 'let'
97
+ : 'var';
98
+ return `${prefix}${declarationKind} ${collectVariableNames(statement.declarationList).join(', ')}`;
99
+ }
100
+
101
+ if (ts.isExportAssignment(statement)) {
102
+ return `export default ${statement.expression.getText(sourceFile)}`;
103
+ }
104
+
105
+ return statement.getText(sourceFile).split('\n')[0];
106
+ };
107
+
108
+ const collectHooks = (sourceFile) => {
109
+ const hooks = new Set();
110
+
111
+ const visit = (node) => {
112
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && /^use[A-Z]/.test(node.expression.text)) {
113
+ hooks.add(node.expression.text);
114
+ }
115
+
116
+ ts.forEachChild(node, visit);
117
+ };
118
+
119
+ ts.forEachChild(sourceFile, visit);
120
+
121
+ return [...hooks].sort();
122
+ };
123
+
124
+ const parseSource = (fullPath, content) => {
125
+ const extension = path.extname(fullPath).toLowerCase();
126
+ const scriptKind = scriptKindByExtension[extension] ?? ts.ScriptKind.TS;
127
+ return ts.createSourceFile(fullPath, content, ts.ScriptTarget.Latest, true, scriptKind);
128
+ };
129
+
130
+ const getDeclarationName = (statement) => {
131
+ if (ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement) ||
132
+ ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement) ||
133
+ ts.isEnumDeclaration(statement)) {
134
+ return statement.name?.text ?? null;
135
+ }
136
+
137
+ if (ts.isVariableStatement(statement)) {
138
+ return statement.declarationList.declarations.map((d) => formatDeclarationName(d.name));
139
+ }
140
+
141
+ return null;
142
+ };
143
+
144
+ const matchesSymbolName = (statement, symbolName) => {
145
+ const name = getDeclarationName(statement);
146
+ if (Array.isArray(name)) return name.includes(symbolName);
147
+ return name === symbolName;
148
+ };
149
+
150
+ const nodeToNumberedLines = (node, sourceFile, content) => {
151
+ const startPos = node.getStart(sourceFile);
152
+ const endPos = node.getEnd();
153
+ const { line: startLine } = sourceFile.getLineAndCharacterOfPosition(startPos);
154
+ const { line: endLine } = sourceFile.getLineAndCharacterOfPosition(endPos);
155
+ const lines = content.split('\n').slice(startLine, endLine + 1);
156
+ return lines.map((l, i) => `${startLine + i + 1}|${l}`).join('\n');
157
+ };
158
+
159
+ const getNodeIdentifierName = (node) => {
160
+ if (node.name && ts.isIdentifier(node.name)) return node.name.text;
161
+ if (node.name && ts.isComputedPropertyName(node.name)) return null;
162
+ return null;
163
+ };
164
+
165
+ const findSymbolNode = (node, symbolName) => {
166
+ if (ts.isMethodDeclaration(node) || ts.isPropertyDeclaration(node) ||
167
+ ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node)) {
168
+ if (getNodeIdentifierName(node) === symbolName) return node;
169
+ }
170
+
171
+ if (ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node)) {
172
+ if (getNodeIdentifierName(node) === symbolName) return node;
173
+ }
174
+
175
+ if (ts.isMethodSignature(node) || ts.isPropertySignature(node)) {
176
+ if (getNodeIdentifierName(node) === symbolName) return node;
177
+ }
178
+
179
+ let found = null;
180
+ ts.forEachChild(node, (child) => {
181
+ if (!found) found = findSymbolNode(child, symbolName);
182
+ });
183
+ return found;
184
+ };
185
+
186
+ export const extractCodeSymbol = (fullPath, content, symbolName) => {
187
+ const sourceFile = parseSource(fullPath, content);
188
+
189
+ for (const statement of sourceFile.statements) {
190
+ if (matchesSymbolName(statement, symbolName)) {
191
+ return nodeToNumberedLines(statement, sourceFile, content);
192
+ }
193
+ }
194
+
195
+ const nested = findSymbolNode(sourceFile, symbolName);
196
+ if (nested) {
197
+ return nodeToNumberedLines(nested, sourceFile, content);
198
+ }
199
+
200
+ return `Symbol not found: ${symbolName}`;
201
+ };
202
+
203
+ export const summarizeCode = (fullPath, content, mode) => {
204
+ const sourceFile = parseSource(fullPath, content);
205
+ const topLevel = sourceFile.statements.map((statement) => formatTopLevelStatement(statement, sourceFile));
206
+ const hooks = collectHooks(sourceFile);
207
+
208
+ if (mode === 'signatures') {
209
+ return truncateSection('# Signatures', toUniqueLines(topLevel), 4000);
210
+ }
211
+
212
+ return joinSections([
213
+ truncateSection('# Outline', toUniqueLines(topLevel), 4000),
214
+ truncateSection('# Hooks', hooks, 1200),
215
+ ], 5000);
216
+ };
@@ -0,0 +1,23 @@
1
+ import { truncate } from '../../utils/text.js';
2
+
3
+ export const summarizeFallback = (content, mode) => {
4
+ const lines = content.split('\n');
5
+ const matches = lines.filter((line) => {
6
+ const trimmed = line.trim();
7
+
8
+ return (
9
+ trimmed.startsWith('import ') ||
10
+ trimmed.startsWith('export ') ||
11
+ trimmed.includes('function ') ||
12
+ trimmed.includes('const ') ||
13
+ trimmed.includes('return (') ||
14
+ trimmed.includes('class ') ||
15
+ trimmed.includes('def ') ||
16
+ trimmed.includes('resource ') ||
17
+ trimmed.includes('FROM ') ||
18
+ trimmed.includes('fn ')
19
+ );
20
+ });
21
+
22
+ return truncate(matches.join('\n') || content, mode === 'signatures' ? 4000 : 5000);
23
+ };
@@ -0,0 +1,178 @@
1
+ import { joinSections, toUniqueLines, truncateSection } from './shared.js';
2
+
3
+ export const extractPythonSymbol = (content, symbolName) => {
4
+ const lines = content.split('\n');
5
+ let startIdx = -1;
6
+ let baseIndent = -1;
7
+
8
+ for (let i = 0; i < lines.length; i++) {
9
+ const trimmed = lines[i].trim();
10
+ const indent = lines[i].length - lines[i].trimStart().length;
11
+
12
+ const match = trimmed.match(/^(?:async\s+)?(?:def|class)\s+([A-Za-z_][A-Za-z0-9_]*)/);
13
+ if (match && match[1] === symbolName) {
14
+ startIdx = i;
15
+ baseIndent = indent;
16
+ break;
17
+ }
18
+ }
19
+
20
+ if (startIdx === -1) return `Symbol not found: ${symbolName}`;
21
+
22
+ let endIdx = startIdx + 1;
23
+ while (endIdx < lines.length) {
24
+ const line = lines[endIdx];
25
+ const trimmed = line.trim();
26
+ if (trimmed && !trimmed.startsWith('#')) {
27
+ const indent = line.length - line.trimStart().length;
28
+ if (indent <= baseIndent) break;
29
+ }
30
+ endIdx++;
31
+ }
32
+
33
+ const slice = lines.slice(startIdx, endIdx);
34
+ return slice.map((l, i) => `${startIdx + i + 1}|${l}`).join('\n');
35
+ };
36
+
37
+ export const summarizePython = (content, mode) => {
38
+ const lines = content.split('\n');
39
+ const imports = [];
40
+ const topLevelDefs = [];
41
+ const classNames = [];
42
+ const classMethods = new Map();
43
+ const constants = [];
44
+ let currentClass = null;
45
+ let currentClassIndent = -1;
46
+ let pendingDecorators = 0;
47
+ let pendingHeader = null;
48
+
49
+ const normalizeHeader = (parts) => parts.join(' ').replace(/\s+/g, ' ').trim();
50
+
51
+ const finalizePendingHeader = () => {
52
+ if (!pendingHeader) {
53
+ return;
54
+ }
55
+
56
+ const header = normalizeHeader(pendingHeader.parts);
57
+
58
+ if (pendingHeader.kind === 'class') {
59
+ const classMatch = header.match(/^class\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:\((.*?)\))?\s*:/);
60
+ const className = classMatch?.[1] ?? header.replace(/:$/, '');
61
+ const bases = classMatch?.[2] ? `(${classMatch[2].trim()})` : '';
62
+ const prefix = pendingHeader.decorated ? '@decorated ' : '';
63
+ const classLabel = `${prefix}class ${className}${bases}`;
64
+
65
+ classNames.push(classLabel);
66
+ if (!classMethods.has(classLabel)) {
67
+ classMethods.set(classLabel, []);
68
+ }
69
+ currentClass = classLabel;
70
+ currentClassIndent = pendingHeader.indent;
71
+ }
72
+
73
+ if (pendingHeader.kind === 'function') {
74
+ const functionMatch = header.match(/^(async\s+def|def)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\((.*)\)\s*(?:->\s*([^:]+))?\s*:/);
75
+ const functionKind = functionMatch?.[1] ?? 'def';
76
+ const functionName = functionMatch?.[2] ?? 'anonymous';
77
+ const params = (functionMatch?.[3] ?? '').trim();
78
+ const returnType = functionMatch?.[4]?.trim();
79
+ const prefix = pendingHeader.decorated ? '@decorated ' : '';
80
+ const asyncPrefix = functionKind === 'async def' ? 'async ' : '';
81
+ const returnSuffix = returnType ? ` -> ${returnType}` : '';
82
+ const signature = `${prefix}${asyncPrefix}def ${functionName}(${params})${returnSuffix}`;
83
+
84
+ if (currentClass && pendingHeader.indent > currentClassIndent) {
85
+ classMethods.get(currentClass).push(signature);
86
+ } else {
87
+ topLevelDefs.push(signature);
88
+ }
89
+ }
90
+
91
+ pendingHeader = null;
92
+ pendingDecorators = 0;
93
+ };
94
+
95
+ for (const rawLine of lines) {
96
+ const trimmed = rawLine.trim();
97
+ const indent = rawLine.length - rawLine.trimStart().length;
98
+
99
+ if (!trimmed || trimmed.startsWith('#')) {
100
+ continue;
101
+ }
102
+
103
+ if (pendingHeader) {
104
+ pendingHeader.parts.push(trimmed);
105
+ if (trimmed.endsWith(':')) {
106
+ finalizePendingHeader();
107
+ }
108
+ continue;
109
+ }
110
+
111
+ if (currentClass && indent <= currentClassIndent && !trimmed.startsWith('@')) {
112
+ currentClass = null;
113
+ currentClassIndent = -1;
114
+ }
115
+
116
+ const importMatch = rawLine.match(/^\s*(import\s+.+|from\s+.+\s+import\s+.+)$/);
117
+ if (importMatch) {
118
+ imports.push(importMatch[1].trim());
119
+ pendingDecorators = 0;
120
+ continue;
121
+ }
122
+
123
+ if (/^\s*@/.test(rawLine)) {
124
+ pendingDecorators += 1;
125
+ continue;
126
+ }
127
+
128
+ if (/^\s*class\s+[A-Za-z_][A-Za-z0-9_]*/.test(rawLine)) {
129
+ pendingHeader = { kind: 'class', indent, parts: [trimmed], decorated: pendingDecorators > 0 };
130
+ if (trimmed.endsWith(':')) {
131
+ finalizePendingHeader();
132
+ }
133
+ continue;
134
+ }
135
+
136
+ if (/^\s*(async\s+def|def)\s+[A-Za-z_][A-Za-z0-9_]*\s*\(/.test(rawLine)) {
137
+ pendingHeader = { kind: 'function', indent, parts: [trimmed], decorated: pendingDecorators > 0 };
138
+ if (trimmed.endsWith(':')) {
139
+ finalizePendingHeader();
140
+ }
141
+ continue;
142
+ }
143
+
144
+ const constantMatch = rawLine.match(/^\s*([A-Z][A-Z0-9_]+)\s*=/);
145
+ if (constantMatch) {
146
+ constants.push(`const ${constantMatch[1]}`);
147
+ }
148
+
149
+ pendingDecorators = 0;
150
+ }
151
+
152
+ finalizePendingHeader();
153
+
154
+ const importLines = toUniqueLines(imports, mode === 'signatures' ? 6 : 12);
155
+ const topLevelLines = toUniqueLines(topLevelDefs, 12);
156
+ const classLines = toUniqueLines(classNames, 12);
157
+ const methodLines = [...classMethods.entries()].flatMap(([classLabel, methods]) => {
158
+ const limitedMethods = toUniqueLines(methods, 8);
159
+ return limitedMethods.length === 0 ? [classLabel] : [classLabel, ...limitedMethods.map((method) => ` ${method}`)];
160
+ });
161
+ const constantLines = toUniqueLines(constants, 12);
162
+
163
+ if (mode === 'signatures') {
164
+ return joinSections([
165
+ truncateSection('# Classes and methods', methodLines.length > 0 ? methodLines : classLines, 2200),
166
+ truncateSection('# Top-level definitions', topLevelLines, 900),
167
+ truncateSection('# Imports', importLines, 700),
168
+ ], 4000);
169
+ }
170
+
171
+ return joinSections([
172
+ truncateSection('# Classes and methods', methodLines, 2200),
173
+ truncateSection('# Classes', classLines, 900),
174
+ truncateSection('# Top-level definitions', topLevelLines, 1200),
175
+ truncateSection('# Imports', importLines, 1200),
176
+ truncateSection('# Constants', constantLines, 800),
177
+ ], 5000);
178
+ };
@@ -0,0 +1,39 @@
1
+ import { truncate, uniqueLines } from '../../utils/text.js';
2
+
3
+ export const toUniqueLines = (items, limit = Infinity) => uniqueLines(items.join('\n')).split('\n').filter(Boolean).slice(0, limit);
4
+
5
+ export const truncateSection = (title, lines, maxChars) => {
6
+ if (lines.length === 0) {
7
+ return '';
8
+ }
9
+
10
+ return truncate(`${title}\n${lines.join('\n')}`, maxChars);
11
+ };
12
+
13
+ export const joinSections = (sections, maxChars) => truncate(sections.filter(Boolean).join('\n\n'), maxChars);
14
+
15
+ export const summarizeJson = (content, mode) => {
16
+ try {
17
+ const parsed = JSON.parse(content);
18
+ const entries = Object.entries(parsed ?? {});
19
+ const lines = entries.map(([key, value]) => {
20
+ if (Array.isArray(value)) {
21
+ return `${key}: array(${value.length})`;
22
+ }
23
+
24
+ if (value && typeof value === 'object') {
25
+ return `${key}: object(${Object.keys(value).length})`;
26
+ }
27
+
28
+ if (typeof value === 'string') {
29
+ return `${key}: ${value.slice(0, 48)}`;
30
+ }
31
+
32
+ return `${key}: ${String(value)}`;
33
+ });
34
+
35
+ return truncateSection('# JSON outline', lines, mode === 'signatures' ? 4000 : 5000);
36
+ } catch {
37
+ return truncate(content, 4000);
38
+ }
39
+ };
@@ -0,0 +1,72 @@
1
+ import { joinSections, toUniqueLines, truncateSection } from './shared.js';
2
+
3
+ export const summarizeToml = (content, mode) => {
4
+ const lines = content.split('\n');
5
+ const sections = [];
6
+ const keys = [];
7
+ let currentSection = 'root';
8
+
9
+ for (const rawLine of lines) {
10
+ const line = rawLine.trim();
11
+
12
+ if (!line || line.startsWith('#')) {
13
+ continue;
14
+ }
15
+
16
+ const sectionMatch = line.match(/^\[\[?([^\]]+)\]\]?$/);
17
+ if (sectionMatch) {
18
+ currentSection = sectionMatch[1];
19
+ sections.push(`[${currentSection}]`);
20
+ continue;
21
+ }
22
+
23
+ const keyMatch = line.match(/^([A-Za-z0-9_.-]+)\s*=/);
24
+ if (keyMatch) {
25
+ keys.push(`${currentSection}.${keyMatch[1]}`);
26
+ }
27
+ }
28
+
29
+ if (mode === 'signatures') {
30
+ return truncateSection('# TOML sections', toUniqueLines(sections), 4000);
31
+ }
32
+
33
+ return joinSections([
34
+ truncateSection('# TOML sections', toUniqueLines(sections), 1800),
35
+ truncateSection('# Keys', toUniqueLines(keys), 2800),
36
+ ], 5000);
37
+ };
38
+
39
+ export const summarizeYaml = (content, mode) => {
40
+ const lines = content.split('\n');
41
+ const sections = [];
42
+ const nestedKeys = [];
43
+
44
+ for (const rawLine of lines) {
45
+ if (!rawLine.trim() || rawLine.trim().startsWith('#')) {
46
+ continue;
47
+ }
48
+
49
+ const keyMatch = rawLine.match(/^(\s*)([^\s:#][^:]*):/);
50
+ if (!keyMatch) {
51
+ continue;
52
+ }
53
+
54
+ const indent = keyMatch[1].length / 2;
55
+ const key = keyMatch[2].trim();
56
+
57
+ if (indent === 0) {
58
+ sections.push(key);
59
+ } else {
60
+ nestedKeys.push(`${' '.repeat(indent)}${key}`);
61
+ }
62
+ }
63
+
64
+ if (mode === 'signatures') {
65
+ return truncateSection('# YAML keys', toUniqueLines(sections), 4000);
66
+ }
67
+
68
+ return joinSections([
69
+ truncateSection('# YAML top-level', toUniqueLines(sections), 1600),
70
+ truncateSection('# YAML nested', toUniqueLines(nestedKeys), 2800),
71
+ ], 5000);
72
+ };
@@ -0,0 +1,63 @@
1
+ import { smartRead } from './smart-read.js';
2
+ import { countTokens } from '../tokenCounter.js';
3
+
4
+ export const smartReadBatch = async ({ files, maxTokens }) => {
5
+ const results = [];
6
+ let totalTokens = 0;
7
+ let totalRawTokens = 0;
8
+ let filesSkipped = 0;
9
+
10
+ for (const item of files) {
11
+ try {
12
+ const readResult = await smartRead({
13
+ filePath: item.path,
14
+ mode: item.mode,
15
+ symbol: item.symbol,
16
+ startLine: item.startLine,
17
+ endLine: item.endLine,
18
+ maxTokens: item.maxTokens,
19
+ });
20
+
21
+ const itemTokens = countTokens(readResult.content);
22
+
23
+ if (maxTokens && totalTokens + itemTokens > maxTokens && results.length > 0) {
24
+ filesSkipped = files.length - results.length;
25
+ break;
26
+ }
27
+
28
+ results.push({
29
+ filePath: readResult.filePath,
30
+ mode: readResult.mode,
31
+ parser: readResult.parser,
32
+ truncated: readResult.truncated,
33
+ content: readResult.content,
34
+ ...(readResult.confidence ? { confidence: readResult.confidence } : {}),
35
+ ...(readResult.indexHint !== undefined ? { indexHint: readResult.indexHint } : {}),
36
+ ...(readResult.chosenMode ? { chosenMode: readResult.chosenMode, budgetApplied: true } : {}),
37
+ });
38
+
39
+ totalTokens += itemTokens;
40
+ totalRawTokens += readResult.metrics?.rawTokens ?? 0;
41
+ } catch (err) {
42
+ results.push({
43
+ filePath: item.path,
44
+ mode: item.mode ?? 'outline',
45
+ error: err.message || 'Failed to read file',
46
+ });
47
+ }
48
+ }
49
+
50
+ const totalSavingsPct = totalRawTokens > 0
51
+ ? Math.max(0, Math.round(((totalRawTokens - totalTokens) / totalRawTokens) * 100))
52
+ : 0;
53
+
54
+ return {
55
+ results,
56
+ metrics: {
57
+ totalTokens,
58
+ filesRead: results.length,
59
+ filesSkipped,
60
+ totalSavingsPct,
61
+ },
62
+ };
63
+ };