tokenlean 0.1.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,123 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * tl-search - Run pre-defined search patterns
5
+ *
6
+ * Looks for searchPatterns in:
7
+ * 1. .tokenleanrc.json in project root
8
+ * 2. ~/.tokenleanrc.json (global)
9
+ *
10
+ * Usage: tl-search <pattern-name>
11
+ */
12
+
13
+ // Prompt info for tl-prompt
14
+ if (process.argv.includes('--prompt')) {
15
+ console.log(JSON.stringify({
16
+ name: 'tl-search',
17
+ desc: 'Run pre-defined search patterns',
18
+ when: 'search',
19
+ example: 'tl-search'
20
+ }));
21
+ process.exit(0);
22
+ }
23
+
24
+ import { spawn, execSync } from 'child_process';
25
+ import { loadConfig, getSearchPatterns, CONFIG_FILENAME } from '../src/config.mjs';
26
+
27
+ // Check for ripgrep
28
+ try {
29
+ execSync('which rg', { stdio: 'ignore' });
30
+ } catch {
31
+ console.error('ripgrep (rg) not found. Install: brew install ripgrep');
32
+ process.exit(1);
33
+ }
34
+
35
+ function showHelp(patterns) {
36
+ const names = Object.keys(patterns);
37
+
38
+ if (names.length === 0) {
39
+ console.log('\nNo search patterns defined.');
40
+ console.log(`\nAdd patterns to ${CONFIG_FILENAME}:`);
41
+ console.log(`
42
+ {
43
+ "searchPatterns": {
44
+ "hooks": {
45
+ "description": "Find lifecycle hooks",
46
+ "pattern": "use(Effect|State|Callback)",
47
+ "glob": "**/*.{ts,tsx}"
48
+ }
49
+ }
50
+ }`);
51
+ process.exit(0);
52
+ }
53
+
54
+ console.log('\nAvailable search patterns:\n');
55
+
56
+ const maxLen = Math.max(...names.map(k => k.length));
57
+
58
+ for (const [name, config] of Object.entries(patterns)) {
59
+ const paddedName = name.padEnd(maxLen);
60
+ console.log(` ${paddedName} ${config.description || '(no description)'}`);
61
+ }
62
+
63
+ console.log('\nUsage: tl-search <pattern-name>');
64
+ console.log('Example: tl-search hooks\n');
65
+ }
66
+
67
+ function runSearch(name, config, rootDir) {
68
+ console.log(`\nSearching: ${config.description || name}`);
69
+ console.log(`Pattern: ${config.pattern}\n`);
70
+
71
+ if (config.type === 'glob-only') {
72
+ const args = ['--files', '-g', config.pattern];
73
+ if (config.exclude) {
74
+ for (const ex of config.exclude) {
75
+ args.push('-g', `!${ex}`);
76
+ }
77
+ }
78
+ args.push(rootDir);
79
+ const proc = spawn('rg', args, { stdio: 'inherit' });
80
+ proc.on('close', code => process.exit(code === 1 ? 0 : code));
81
+ return;
82
+ }
83
+
84
+ const args = ['--color=always', '-n', '-e', config.pattern];
85
+
86
+ if (config.glob) {
87
+ args.push('-g', config.glob);
88
+ }
89
+
90
+ if (config.exclude) {
91
+ for (const ex of config.exclude) {
92
+ args.push('-g', `!${ex}`);
93
+ }
94
+ }
95
+
96
+ args.push(rootDir);
97
+
98
+ const proc = spawn('rg', args, { stdio: 'inherit' });
99
+ proc.on('close', code => {
100
+ if (code === 1) {
101
+ console.log('No matches found.');
102
+ }
103
+ process.exit(code === 1 ? 0 : code);
104
+ });
105
+ }
106
+
107
+ // Main
108
+ const { config, projectRoot } = loadConfig();
109
+ const patterns = config.searchPatterns || {};
110
+ const patternName = process.argv[2];
111
+
112
+ if (!patternName || patternName === '--help' || patternName === '-h') {
113
+ showHelp(patterns);
114
+ process.exit(0);
115
+ }
116
+
117
+ if (!patterns[patternName]) {
118
+ console.error(`\nUnknown pattern: "${patternName}"`);
119
+ showHelp(patterns);
120
+ process.exit(1);
121
+ }
122
+
123
+ runSearch(patternName, patterns[patternName], projectRoot);
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * tl-structure - Smart project overview with context estimates
5
+ *
6
+ * Shows directory structure with token estimates, highlighting
7
+ * important directories and files for quick orientation.
8
+ *
9
+ * Usage: tl-structure [path] [--depth N]
10
+ */
11
+
12
+ import { readdirSync, readFileSync, existsSync } from 'fs';
13
+ import { join, basename } from 'path';
14
+ import { estimateTokens, formatTokens } from '../src/output.mjs';
15
+ import {
16
+ getSkipDirs,
17
+ getImportantFiles,
18
+ getImportantDirs
19
+ } from '../src/project.mjs';
20
+ import { getConfig } from '../src/config.mjs';
21
+
22
+ function getDirStats(dirPath, skipDirs, importantDirs) {
23
+ let totalTokens = 0;
24
+ let fileCount = 0;
25
+
26
+ function walk(dir) {
27
+ try {
28
+ const entries = readdirSync(dir, { withFileTypes: true });
29
+ for (const entry of entries) {
30
+ if (entry.name.startsWith('.') && !importantDirs.has(entry.name)) continue;
31
+ if (skipDirs.has(entry.name)) continue;
32
+
33
+ const fullPath = join(dir, entry.name);
34
+ if (entry.isDirectory()) {
35
+ walk(fullPath);
36
+ } else {
37
+ try {
38
+ const content = readFileSync(fullPath, 'utf-8');
39
+ totalTokens += estimateTokens(content);
40
+ fileCount++;
41
+ } catch (e) { /* skip binary */ }
42
+ }
43
+ }
44
+ } catch (e) { /* permission error */ }
45
+ }
46
+
47
+ walk(dirPath);
48
+ return { totalTokens, fileCount };
49
+ }
50
+
51
+ function printTree(dirPath, prefix, depth, maxDepth, skipDirs, importantDirs, importantFiles) {
52
+ if (depth > maxDepth) return;
53
+
54
+ const entries = readdirSync(dirPath, { withFileTypes: true });
55
+
56
+ // Sort: directories first, then by importance, then alphabetically
57
+ entries.sort((a, b) => {
58
+ if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
59
+ const aImportant = importantDirs.has(a.name) || importantFiles.has(a.name);
60
+ const bImportant = importantDirs.has(b.name) || importantFiles.has(b.name);
61
+ if (aImportant !== bImportant) return aImportant ? -1 : 1;
62
+ return a.name.localeCompare(b.name);
63
+ });
64
+
65
+ const filtered = entries.filter(e => {
66
+ if (e.name.startsWith('.') && e.name !== '.claude') return false;
67
+ if (skipDirs.has(e.name)) return false;
68
+ return true;
69
+ });
70
+
71
+ filtered.forEach((entry, index) => {
72
+ const isLast = index === filtered.length - 1;
73
+ const connector = isLast ? '└── ' : '├── ';
74
+ const fullPath = join(dirPath, entry.name);
75
+
76
+ const isEntryImportant = importantDirs.has(entry.name) || importantFiles.has(entry.name);
77
+ const marker = isEntryImportant ? '*' : ' ';
78
+
79
+ if (entry.isDirectory()) {
80
+ const stats = getDirStats(fullPath, skipDirs, importantDirs);
81
+ const sizeInfo = stats.fileCount > 0
82
+ ? ` (${stats.fileCount} files, ~${formatTokens(stats.totalTokens)})`
83
+ : ' (empty)';
84
+
85
+ console.log(`${prefix}${connector}${marker}${entry.name}/${sizeInfo}`);
86
+
87
+ const newPrefix = prefix + (isLast ? ' ' : '│ ');
88
+ printTree(fullPath, newPrefix, depth + 1, maxDepth, skipDirs, importantDirs, importantFiles);
89
+ } else {
90
+ try {
91
+ const content = readFileSync(fullPath, 'utf-8');
92
+ const tokens = estimateTokens(content);
93
+ const lines = content.split('\n').length;
94
+ console.log(`${prefix}${connector}${marker}${entry.name} (~${formatTokens(tokens)}, ${lines}L)`);
95
+ } catch (e) {
96
+ console.log(`${prefix}${connector}${marker}${entry.name} (binary)`);
97
+ }
98
+ }
99
+ });
100
+ }
101
+
102
+ // Prompt info for tl-prompt
103
+ if (process.argv.includes('--prompt')) {
104
+ console.log(JSON.stringify({
105
+ name: 'tl-structure',
106
+ desc: 'Project overview with token estimates',
107
+ when: 'before-read',
108
+ example: 'tl-structure'
109
+ }));
110
+ process.exit(0);
111
+ }
112
+
113
+ // Main
114
+ const args = process.argv.slice(2);
115
+
116
+ // Get config defaults
117
+ const structureConfig = getConfig('structure') || {};
118
+
119
+ let targetPath = '.';
120
+ let maxDepth = structureConfig.depth || 3;
121
+
122
+ // Get combined sets (defaults + user config extensions)
123
+ const skipDirs = getSkipDirs();
124
+ const importantDirs = getImportantDirs();
125
+ const importantFiles = getImportantFiles();
126
+
127
+ for (let i = 0; i < args.length; i++) {
128
+ if ((args[i] === '--depth' || args[i] === '-d') && args[i + 1]) {
129
+ maxDepth = parseInt(args[i + 1], 10);
130
+ i++;
131
+ } else if (args[i] === '--help' || args[i] === '-h') {
132
+ console.log(`
133
+ tl-structure - Smart project overview with context estimates
134
+
135
+ Usage: tl-structure [path] [options]
136
+
137
+ Options:
138
+ --depth N, -d N Maximum depth to show (default: ${structureConfig.depth || 3})
139
+ --help, -h Show this help
140
+
141
+ Configure defaults in .tokenleanrc.json:
142
+ "structure": { "depth": 3, "important": ["src", "lib"] }
143
+ `);
144
+ process.exit(0);
145
+ } else if (!args[i].startsWith('-')) {
146
+ targetPath = args[i];
147
+ }
148
+ }
149
+
150
+ if (!existsSync(targetPath)) {
151
+ console.error(`Path not found: ${targetPath}`);
152
+ process.exit(1);
153
+ }
154
+
155
+ const rootStats = getDirStats(targetPath, skipDirs, importantDirs);
156
+ console.log(`\n${targetPath === '.' ? basename(process.cwd()) : targetPath}`);
157
+ console.log(`Total: ${rootStats.fileCount} files, ~${formatTokens(rootStats.totalTokens)} tokens`);
158
+ console.log(`(* = important for understanding project)\n`);
159
+
160
+ printTree(targetPath, '', 0, maxDepth, skipDirs, importantDirs, importantFiles);
161
+ console.log();
@@ -0,0 +1,430 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * tl-symbols - Extract function/class/type signatures without bodies
5
+ *
6
+ * Shows the API surface of a file in minimal tokens - signatures only,
7
+ * no implementation details. Perfect for understanding what a file
8
+ * provides without reading the whole thing.
9
+ *
10
+ * Usage: tl-symbols <file> [--exports-only]
11
+ */
12
+
13
+ // Prompt info for tl-prompt
14
+ if (process.argv.includes('--prompt')) {
15
+ console.log(JSON.stringify({
16
+ name: 'tl-symbols',
17
+ desc: 'Function/class signatures without bodies',
18
+ when: 'before-read',
19
+ example: 'tl-symbols src/utils.ts'
20
+ }));
21
+ process.exit(0);
22
+ }
23
+
24
+ import { readFileSync, existsSync } from 'fs';
25
+ import { basename, extname } from 'path';
26
+ import {
27
+ createOutput,
28
+ parseCommonArgs,
29
+ estimateTokens,
30
+ formatTokens,
31
+ COMMON_OPTIONS_HELP
32
+ } from '../src/output.mjs';
33
+
34
+ const HELP = `
35
+ tl-symbols - Extract function/class/type signatures without bodies
36
+
37
+ Usage: tl-symbols <file> [options]
38
+
39
+ Options:
40
+ --exports-only, -e Show only exported symbols
41
+ ${COMMON_OPTIONS_HELP}
42
+
43
+ Examples:
44
+ tl-symbols src/api.ts # All symbols
45
+ tl-symbols src/api.ts -e # Exports only
46
+ tl-symbols src/api.ts -l 20 # Limit to 20 lines
47
+ tl-symbols src/api.ts -j # JSON output
48
+
49
+ Supported languages:
50
+ JavaScript/TypeScript (.js, .ts, .jsx, .tsx, .mjs)
51
+ Python (.py)
52
+ Go (.go)
53
+ `;
54
+
55
+ // ─────────────────────────────────────────────────────────────
56
+ // Language Detection
57
+ // ─────────────────────────────────────────────────────────────
58
+
59
+ const LANG_EXTENSIONS = {
60
+ js: ['.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx', '.mts'],
61
+ python: ['.py'],
62
+ go: ['.go']
63
+ };
64
+
65
+ function detectLanguage(filePath) {
66
+ const ext = extname(filePath).toLowerCase();
67
+ for (const [lang, exts] of Object.entries(LANG_EXTENSIONS)) {
68
+ if (exts.includes(ext)) return lang;
69
+ }
70
+ return null;
71
+ }
72
+
73
+ // ─────────────────────────────────────────────────────────────
74
+ // JavaScript/TypeScript Extraction
75
+ // ─────────────────────────────────────────────────────────────
76
+
77
+ function extractJsSymbols(content, exportsOnly = false) {
78
+ const symbols = {
79
+ exports: [],
80
+ classes: [],
81
+ functions: [],
82
+ types: [],
83
+ constants: []
84
+ };
85
+
86
+ const lines = content.split('\n');
87
+ let inClass = null;
88
+ let braceDepth = 0;
89
+ let currentClassMethods = [];
90
+
91
+ for (let i = 0; i < lines.length; i++) {
92
+ const line = lines[i];
93
+ const trimmed = line.trim();
94
+
95
+ // Skip comments and empty lines
96
+ if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*') || !trimmed) {
97
+ continue;
98
+ }
99
+
100
+ // Track brace depth for class scope
101
+ const openBraces = (line.match(/\{/g) || []).length;
102
+ const closeBraces = (line.match(/\}/g) || []).length;
103
+
104
+ // Check if we're exiting a class
105
+ if (inClass && braceDepth === 1 && closeBraces > openBraces) {
106
+ symbols.classes.push({
107
+ signature: inClass,
108
+ methods: currentClassMethods
109
+ });
110
+ inClass = null;
111
+ currentClassMethods = [];
112
+ }
113
+
114
+ braceDepth += openBraces - closeBraces;
115
+
116
+ // Export statements
117
+ if (trimmed.startsWith('export ')) {
118
+ if (trimmed.includes('export default')) {
119
+ const match = trimmed.match(/export\s+default\s+(?:class|function|async\s+function)?\s*(\w+)?/);
120
+ if (match) {
121
+ symbols.exports.push(trimmed.replace(/\s*\{.*$/, '').trim());
122
+ }
123
+ }
124
+ else if (trimmed.match(/export\s+\{[^}]+\}\s+from/)) {
125
+ symbols.exports.push(trimmed);
126
+ }
127
+ else if (trimmed.match(/export\s+\*\s+from/)) {
128
+ symbols.exports.push(trimmed);
129
+ }
130
+ else if (trimmed.match(/export\s+(interface|type)\s+/)) {
131
+ const sig = extractSignatureLine(trimmed);
132
+ symbols.types.push(sig);
133
+ symbols.exports.push(sig);
134
+ }
135
+ else if (trimmed.match(/export\s+(?:abstract\s+)?class\s+/)) {
136
+ const sig = extractSignatureLine(trimmed);
137
+ inClass = sig;
138
+ currentClassMethods = [];
139
+ braceDepth = openBraces - closeBraces;
140
+ }
141
+ else if (trimmed.match(/export\s+(?:async\s+)?function\s+/)) {
142
+ const sig = extractSignatureLine(trimmed);
143
+ symbols.functions.push(sig);
144
+ symbols.exports.push(sig);
145
+ }
146
+ else if (trimmed.match(/export\s+const\s+/)) {
147
+ const sig = extractSignatureLine(trimmed);
148
+ if (trimmed.includes('=>') || trimmed.match(/:\s*\([^)]*\)\s*=>/)) {
149
+ symbols.functions.push(sig);
150
+ } else {
151
+ symbols.constants.push(sig);
152
+ }
153
+ symbols.exports.push(sig);
154
+ }
155
+ else if (trimmed.match(/export\s+(?:const\s+)?enum\s+/)) {
156
+ const sig = extractSignatureLine(trimmed);
157
+ symbols.types.push(sig);
158
+ symbols.exports.push(sig);
159
+ }
160
+ }
161
+ // Non-exported symbols
162
+ else if (!exportsOnly) {
163
+ if (trimmed.match(/^interface\s+/)) {
164
+ symbols.types.push(extractSignatureLine(trimmed));
165
+ }
166
+ else if (trimmed.match(/^type\s+\w+/)) {
167
+ symbols.types.push(extractSignatureLine(trimmed));
168
+ }
169
+ else if (trimmed.match(/^(?:abstract\s+)?class\s+/)) {
170
+ const sig = extractSignatureLine(trimmed);
171
+ inClass = sig;
172
+ currentClassMethods = [];
173
+ braceDepth = openBraces - closeBraces;
174
+ }
175
+ else if (trimmed.match(/^(?:async\s+)?function\s+/)) {
176
+ symbols.functions.push(extractSignatureLine(trimmed));
177
+ }
178
+ else if (braceDepth === 0 && trimmed.match(/^const\s+\w+.*=.*=>/)) {
179
+ symbols.functions.push(extractSignatureLine(trimmed));
180
+ }
181
+ // Class methods
182
+ else if (inClass && braceDepth >= 1) {
183
+ if (trimmed.match(/^constructor\s*\(/)) {
184
+ currentClassMethods.push(extractSignatureLine(trimmed));
185
+ }
186
+ else if (trimmed.match(/^(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:async\s+)?(?:get\s+|set\s+)?(\w+)\s*[(<]/)) {
187
+ if (!trimmed.includes('=') || trimmed.includes('=>')) {
188
+ currentClassMethods.push(extractSignatureLine(trimmed));
189
+ }
190
+ }
191
+ }
192
+ }
193
+ }
194
+
195
+ // Handle last class if file ends inside one
196
+ if (inClass) {
197
+ symbols.classes.push({
198
+ signature: inClass,
199
+ methods: currentClassMethods
200
+ });
201
+ }
202
+
203
+ return symbols;
204
+ }
205
+
206
+ function extractSignatureLine(line) {
207
+ let sig = line
208
+ .replace(/\s*\{[\s\S]*$/, '')
209
+ .replace(/\s*=>\s*[^{].*$/, ' =>')
210
+ .replace(/\s*=\s*[^=].*$/, '')
211
+ .trim();
212
+
213
+ sig = sig.replace(/[,;]$/, '').trim();
214
+ return sig;
215
+ }
216
+
217
+ // ─────────────────────────────────────────────────────────────
218
+ // Python Extraction
219
+ // ─────────────────────────────────────────────────────────────
220
+
221
+ function extractPythonSymbols(content) {
222
+ const symbols = { classes: [], functions: [] };
223
+ const lines = content.split('\n');
224
+ let inClass = null;
225
+ let currentClassMethods = [];
226
+
227
+ for (const line of lines) {
228
+ const trimmed = line.trim();
229
+
230
+ const classMatch = trimmed.match(/^class\s+(\w+)(?:\([^)]*\))?:/);
231
+ if (classMatch) {
232
+ if (inClass) {
233
+ symbols.classes.push({ signature: inClass, methods: currentClassMethods });
234
+ }
235
+ inClass = trimmed.replace(/:$/, '');
236
+ currentClassMethods = [];
237
+ continue;
238
+ }
239
+
240
+ const funcMatch = trimmed.match(/^(?:async\s+)?def\s+(\w+)\s*\([^)]*\)(?:\s*->\s*[^:]+)?:/);
241
+ if (funcMatch) {
242
+ const sig = trimmed.replace(/:$/, '');
243
+ if (inClass && line.startsWith(' ')) {
244
+ currentClassMethods.push(sig);
245
+ } else {
246
+ if (inClass) {
247
+ symbols.classes.push({ signature: inClass, methods: currentClassMethods });
248
+ inClass = null;
249
+ currentClassMethods = [];
250
+ }
251
+ symbols.functions.push(sig);
252
+ }
253
+ }
254
+ }
255
+
256
+ if (inClass) {
257
+ symbols.classes.push({ signature: inClass, methods: currentClassMethods });
258
+ }
259
+
260
+ return symbols;
261
+ }
262
+
263
+ // ─────────────────────────────────────────────────────────────
264
+ // Go Extraction
265
+ // ─────────────────────────────────────────────────────────────
266
+
267
+ function extractGoSymbols(content) {
268
+ const symbols = { types: [], functions: [] };
269
+ const lines = content.split('\n');
270
+
271
+ for (const line of lines) {
272
+ const trimmed = line.trim();
273
+
274
+ if (trimmed.match(/^type\s+\w+\s+(?:struct|interface)/)) {
275
+ symbols.types.push(trimmed.replace(/\s*\{.*$/, ''));
276
+ }
277
+
278
+ if (trimmed.match(/^func\s+/)) {
279
+ symbols.functions.push(trimmed.replace(/\s*\{.*$/, ''));
280
+ }
281
+ }
282
+
283
+ return symbols;
284
+ }
285
+
286
+ // ─────────────────────────────────────────────────────────────
287
+ // Formatting
288
+ // ─────────────────────────────────────────────────────────────
289
+
290
+ function formatSymbols(symbols, lang, out) {
291
+ if (lang === 'js') {
292
+ if (symbols.exports.length > 0) {
293
+ out.add('Exports:');
294
+ const unique = [...new Set(symbols.exports)];
295
+ unique.forEach(e => out.add(' ' + e));
296
+ out.blank();
297
+ }
298
+
299
+ if (symbols.classes.length > 0) {
300
+ out.add('Classes:');
301
+ for (const cls of symbols.classes) {
302
+ out.add(' ' + cls.signature);
303
+ cls.methods.forEach(m => out.add(' ' + m));
304
+ }
305
+ out.blank();
306
+ }
307
+
308
+ const nonExportedFuncs = symbols.functions.filter(f => !f.startsWith('export'));
309
+ if (nonExportedFuncs.length > 0) {
310
+ out.add('Functions:');
311
+ nonExportedFuncs.forEach(f => out.add(' ' + f));
312
+ out.blank();
313
+ }
314
+
315
+ const nonExportedTypes = symbols.types.filter(t => !t.startsWith('export'));
316
+ if (nonExportedTypes.length > 0) {
317
+ out.add('Types:');
318
+ nonExportedTypes.forEach(t => out.add(' ' + t));
319
+ out.blank();
320
+ }
321
+
322
+ const nonExportedConsts = symbols.constants.filter(c => !c.startsWith('export'));
323
+ if (nonExportedConsts.length > 0) {
324
+ out.add('Constants:');
325
+ nonExportedConsts.forEach(c => out.add(' ' + c));
326
+ out.blank();
327
+ }
328
+ } else if (lang === 'python') {
329
+ if (symbols.classes.length > 0) {
330
+ out.add('Classes:');
331
+ for (const cls of symbols.classes) {
332
+ out.add(' ' + cls.signature);
333
+ cls.methods.forEach(m => out.add(' ' + m));
334
+ }
335
+ out.blank();
336
+ }
337
+
338
+ if (symbols.functions.length > 0) {
339
+ out.add('Functions:');
340
+ symbols.functions.forEach(f => out.add(' ' + f));
341
+ out.blank();
342
+ }
343
+ } else if (lang === 'go') {
344
+ if (symbols.types.length > 0) {
345
+ out.add('Types:');
346
+ symbols.types.forEach(t => out.add(' ' + t));
347
+ out.blank();
348
+ }
349
+
350
+ if (symbols.functions.length > 0) {
351
+ out.add('Functions:');
352
+ symbols.functions.forEach(f => out.add(' ' + f));
353
+ out.blank();
354
+ }
355
+ }
356
+ }
357
+
358
+ function countSymbols(symbols) {
359
+ let count = 0;
360
+ if (symbols.exports) count += symbols.exports.length;
361
+ if (symbols.classes) {
362
+ count += symbols.classes.length;
363
+ symbols.classes.forEach(c => count += c.methods?.length || 0);
364
+ }
365
+ if (symbols.functions) count += symbols.functions.length;
366
+ if (symbols.types) count += symbols.types.length;
367
+ if (symbols.constants) count += symbols.constants.length;
368
+ return count;
369
+ }
370
+
371
+ // ─────────────────────────────────────────────────────────────
372
+ // Main
373
+ // ─────────────────────────────────────────────────────────────
374
+
375
+ const args = process.argv.slice(2);
376
+ const options = parseCommonArgs(args);
377
+ const exportsOnly = options.remaining.includes('--exports-only') || options.remaining.includes('-e');
378
+ const filePath = options.remaining.find(a => !a.startsWith('-'));
379
+
380
+ if (options.help || !filePath) {
381
+ console.log(HELP);
382
+ process.exit(options.help ? 0 : 1);
383
+ }
384
+
385
+ if (!existsSync(filePath)) {
386
+ console.error(`File not found: ${filePath}`);
387
+ process.exit(1);
388
+ }
389
+
390
+ const lang = detectLanguage(filePath);
391
+ if (!lang) {
392
+ console.error(`Unsupported file type: ${extname(filePath)}`);
393
+ console.error('Supported: .js, .ts, .jsx, .tsx, .mjs, .py, .go');
394
+ process.exit(1);
395
+ }
396
+
397
+ const content = readFileSync(filePath, 'utf-8');
398
+ const fullFileTokens = estimateTokens(content);
399
+
400
+ let symbols;
401
+ switch (lang) {
402
+ case 'js':
403
+ symbols = extractJsSymbols(content, exportsOnly);
404
+ break;
405
+ case 'python':
406
+ symbols = extractPythonSymbols(content);
407
+ break;
408
+ case 'go':
409
+ symbols = extractGoSymbols(content);
410
+ break;
411
+ }
412
+
413
+ const symbolCount = countSymbols(symbols);
414
+ const out = createOutput(options);
415
+
416
+ // Set JSON data
417
+ out.setData('file', basename(filePath));
418
+ out.setData('language', lang);
419
+ out.setData('symbolCount', symbolCount);
420
+ out.setData('fullFileTokens', fullFileTokens);
421
+ out.setData('symbols', symbols);
422
+
423
+ // Build text output
424
+ out.header(`\n📦 ${basename(filePath)} (${symbolCount} symbols)`);
425
+ out.header(` Full file: ~${formatTokens(fullFileTokens)} tokens → Symbols only: ~${formatTokens(Math.ceil(symbolCount * 15))} tokens`);
426
+ out.blank();
427
+
428
+ formatSymbols(symbols, lang, out);
429
+
430
+ out.print();