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,274 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * tl-component - React component analyzer
5
+ *
6
+ * Analyzes a React component to show props, hooks, dependencies,
7
+ * and structure without reading the full file.
8
+ *
9
+ * Usage: tl-component <file.tsx>
10
+ */
11
+
12
+ // Prompt info for tl-prompt
13
+ if (process.argv.includes('--prompt')) {
14
+ console.log(JSON.stringify({
15
+ name: 'tl-component',
16
+ desc: 'Analyze React component (props, hooks, imports)',
17
+ when: 'before-read',
18
+ example: 'tl-component src/Button.tsx'
19
+ }));
20
+ process.exit(0);
21
+ }
22
+
23
+ import { readFileSync, existsSync } from 'fs';
24
+ import { join, relative, dirname, basename } from 'path';
25
+
26
+ function findProjectRoot() {
27
+ let dir = process.cwd();
28
+ while (dir !== '/') {
29
+ if (existsSync(join(dir, 'package.json'))) return dir;
30
+ dir = dirname(dir);
31
+ }
32
+ return process.cwd();
33
+ }
34
+
35
+ function extractImports(content) {
36
+ const imports = {
37
+ react: [],
38
+ reactNative: [],
39
+ internal: [],
40
+ external: [],
41
+ types: []
42
+ };
43
+
44
+ const importRegex = /import\s+(?:{([^}]+)}|(\w+))\s+from\s+['"]([^'"]+)['"]/g;
45
+ let match;
46
+
47
+ while ((match = importRegex.exec(content)) !== null) {
48
+ const names = (match[1] || match[2]).split(',').map(s => s.trim());
49
+ const source = match[3];
50
+
51
+ if (source === 'react') {
52
+ imports.react.push(...names);
53
+ } else if (source.startsWith('react-native')) {
54
+ imports.reactNative.push(...names);
55
+ } else if (source.startsWith('.') || source.startsWith('@/')) {
56
+ imports.internal.push({ names, source });
57
+ } else if (source.includes('/types') || names.some(n => n.startsWith('type ') || /^[A-Z].*Props$/.test(n))) {
58
+ imports.types.push({ names, source });
59
+ } else {
60
+ imports.external.push({ names, source });
61
+ }
62
+ }
63
+
64
+ return imports;
65
+ }
66
+
67
+ function extractHooks(content) {
68
+ const hooks = [];
69
+ const hookRegex = /\b(use[A-Z]\w+)\s*\(/g;
70
+ let match;
71
+
72
+ const seen = new Set();
73
+ while ((match = hookRegex.exec(content)) !== null) {
74
+ if (!seen.has(match[1])) {
75
+ seen.add(match[1]);
76
+ hooks.push(match[1]);
77
+ }
78
+ }
79
+
80
+ return hooks;
81
+ }
82
+
83
+ function extractProps(content) {
84
+ // Look for Props interface/type
85
+ const propsMatch = content.match(/(?:interface|type)\s+(\w*Props)\s*(?:=\s*)?{([^}]+)}/);
86
+ if (!propsMatch) return null;
87
+
88
+ const name = propsMatch[1];
89
+ const body = propsMatch[2];
90
+
91
+ const props = [];
92
+ const propRegex = /(\w+)(\?)?:\s*([^;]+)/g;
93
+ let match;
94
+
95
+ while ((match = propRegex.exec(body)) !== null) {
96
+ props.push({
97
+ name: match[1],
98
+ optional: !!match[2],
99
+ type: match[3].trim()
100
+ });
101
+ }
102
+
103
+ return { name, props };
104
+ }
105
+
106
+ function extractComponents(content) {
107
+ const components = [];
108
+
109
+ // Function components
110
+ const funcRegex = /(?:export\s+)?(?:const|function)\s+(\w+)\s*(?::\s*React\.FC)?[^=]*=?\s*(?:\([^)]*\)|[^=])\s*(?:=>|{)/g;
111
+ let match;
112
+
113
+ while ((match = funcRegex.exec(content)) !== null) {
114
+ const name = match[1];
115
+ // Check if it looks like a component (PascalCase, returns JSX)
116
+ if (/^[A-Z]/.test(name) && !name.endsWith('Props') && !name.endsWith('Type')) {
117
+ components.push(name);
118
+ }
119
+ }
120
+
121
+ return [...new Set(components)];
122
+ }
123
+
124
+ function extractStyles(content) {
125
+ const styles = [];
126
+
127
+ // StyleSheet.create
128
+ if (content.includes('StyleSheet.create')) {
129
+ styles.push('StyleSheet');
130
+ }
131
+
132
+ // styled-components / emotion
133
+ if (content.includes('styled.') || content.includes('styled(')) {
134
+ styles.push('styled-components');
135
+ }
136
+
137
+ // Tailwind / NativeWind
138
+ if (content.includes('className=') || content.includes('tw`')) {
139
+ styles.push('Tailwind/NativeWind');
140
+ }
141
+
142
+ // Inline styles
143
+ if (content.match(/style=\{\{/)) {
144
+ styles.push('inline styles');
145
+ }
146
+
147
+ return styles;
148
+ }
149
+
150
+ function extractRedux(content) {
151
+ const redux = {
152
+ selectors: [],
153
+ actions: [],
154
+ dispatch: false
155
+ };
156
+
157
+ // useSelector calls
158
+ const selectorRegex = /useSelector\(\s*(?:\([^)]*\)\s*=>)?\s*(\w+)/g;
159
+ let match;
160
+ while ((match = selectorRegex.exec(content)) !== null) {
161
+ redux.selectors.push(match[1]);
162
+ }
163
+
164
+ // useDispatch
165
+ if (content.includes('useDispatch')) {
166
+ redux.dispatch = true;
167
+ }
168
+
169
+ // dispatch calls
170
+ const dispatchRegex = /dispatch\(\s*(\w+)/g;
171
+ while ((match = dispatchRegex.exec(content)) !== null) {
172
+ redux.actions.push(match[1]);
173
+ }
174
+
175
+ return redux;
176
+ }
177
+
178
+ function printAnalysis(analysis) {
179
+ const { file, lines, tokens, imports, hooks, propsInfo, components, styles, redux } = analysis;
180
+
181
+ console.log(`\n🧩 Component Analysis: ${file}`);
182
+ console.log(` ${lines} lines, ~${tokens} tokens\n`);
183
+
184
+ // Components
185
+ if (components.length > 0) {
186
+ console.log(`📦 Components: ${components.join(', ')}`);
187
+ }
188
+
189
+ // Props
190
+ if (propsInfo) {
191
+ console.log(`\n📋 ${propsInfo.name}:`);
192
+ for (const p of propsInfo.props) {
193
+ const opt = p.optional ? '?' : '';
194
+ console.log(` ${p.name}${opt}: ${p.type}`);
195
+ }
196
+ }
197
+
198
+ // Hooks
199
+ if (hooks.length > 0) {
200
+ console.log(`\n🪝 Hooks: ${hooks.join(', ')}`);
201
+ }
202
+
203
+ // Redux
204
+ if (redux.dispatch || redux.selectors.length > 0) {
205
+ console.log(`\n📦 Redux:`);
206
+ if (redux.selectors.length > 0) {
207
+ console.log(` Selectors: ${redux.selectors.join(', ')}`);
208
+ }
209
+ if (redux.actions.length > 0) {
210
+ console.log(` Actions: ${redux.actions.join(', ')}`);
211
+ }
212
+ }
213
+
214
+ // Imports summary
215
+ console.log(`\n📥 Imports:`);
216
+ if (imports.react.length > 0) {
217
+ console.log(` React: ${imports.react.join(', ')}`);
218
+ }
219
+ if (imports.reactNative.length > 0) {
220
+ console.log(` React Native: ${imports.reactNative.join(', ')}`);
221
+ }
222
+ if (imports.internal.length > 0) {
223
+ console.log(` Internal: ${imports.internal.length} modules`);
224
+ for (const i of imports.internal.slice(0, 5)) {
225
+ console.log(` ${i.source}`);
226
+ }
227
+ if (imports.internal.length > 5) {
228
+ console.log(` ... and ${imports.internal.length - 5} more`);
229
+ }
230
+ }
231
+ if (imports.external.length > 0) {
232
+ console.log(` External: ${imports.external.map(i => i.source).join(', ')}`);
233
+ }
234
+
235
+ // Styles
236
+ if (styles.length > 0) {
237
+ console.log(`\n🎨 Styling: ${styles.join(', ')}`);
238
+ }
239
+
240
+ console.log();
241
+ }
242
+
243
+ // Main
244
+ const args = process.argv.slice(2);
245
+ const targetFile = args[0];
246
+
247
+ if (!targetFile) {
248
+ console.log('\nUsage: claude-component <file.tsx>\n');
249
+ console.log('Analyzes a React component to show props, hooks, and dependencies.');
250
+ process.exit(1);
251
+ }
252
+
253
+ const fullPath = targetFile.startsWith('/') ? targetFile : join(process.cwd(), targetFile);
254
+ if (!existsSync(fullPath)) {
255
+ console.error(`File not found: ${targetFile}`);
256
+ process.exit(1);
257
+ }
258
+
259
+ const content = readFileSync(fullPath, 'utf-8');
260
+ const projectRoot = findProjectRoot();
261
+
262
+ const analysis = {
263
+ file: relative(projectRoot, fullPath),
264
+ lines: content.split('\n').length,
265
+ tokens: Math.ceil(content.length / 4),
266
+ imports: extractImports(content),
267
+ hooks: extractHooks(content),
268
+ propsInfo: extractProps(content),
269
+ components: extractComponents(content),
270
+ styles: extractStyles(content),
271
+ redux: extractRedux(content)
272
+ };
273
+
274
+ printAnalysis(analysis);
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * tl-config - Show and manage tokenlean configuration
5
+ *
6
+ * Usage:
7
+ * tl-config Show current merged config
8
+ * tl-config --paths Show config file locations
9
+ * tl-config --init Create a sample config file
10
+ */
11
+
12
+ // Prompt info for tl-prompt (tl-config is a utility, not for AI agents)
13
+ if (process.argv.includes('--prompt')) {
14
+ process.exit(0); // No output - utility command
15
+ }
16
+
17
+ import { writeFileSync, existsSync } from 'fs';
18
+ import { join } from 'path';
19
+ import {
20
+ loadConfig,
21
+ getConfigPaths,
22
+ CONFIG_FILENAME,
23
+ GLOBAL_CONFIG_PATH
24
+ } from '../src/config.mjs';
25
+
26
+ const SAMPLE_CONFIG = {
27
+ output: {
28
+ maxLines: 100,
29
+ maxTokens: null,
30
+ format: 'text'
31
+ },
32
+ skipDirs: [],
33
+ skipExtensions: [],
34
+ importantDirs: [],
35
+ importantFiles: [],
36
+ searchPatterns: {
37
+ hooks: {
38
+ description: 'Find React hooks',
39
+ pattern: 'use[A-Z]\\w+',
40
+ glob: '**/*.{ts,tsx,js,jsx}'
41
+ },
42
+ todos: {
43
+ description: 'Find TODO comments',
44
+ pattern: 'TODO|FIXME|HACK',
45
+ glob: '**/*.{ts,tsx,js,jsx,mjs}'
46
+ }
47
+ },
48
+ hotspots: {
49
+ days: 90,
50
+ top: 20
51
+ },
52
+ structure: {
53
+ depth: 3
54
+ }
55
+ };
56
+
57
+ const HELP = `
58
+ tl-config - Show and manage tokenlean configuration
59
+
60
+ Usage:
61
+ tl-config Show current merged config
62
+ tl-config --paths Show config file locations
63
+ tl-config --init Create a sample project config
64
+ tl-config --init-global Create a sample global config
65
+
66
+ Config file locations:
67
+ Project: ${CONFIG_FILENAME} (in project root or parent directories)
68
+ Global: ${GLOBAL_CONFIG_PATH}
69
+
70
+ Project config overrides global config.
71
+ `;
72
+
73
+ const args = process.argv.slice(2);
74
+
75
+ if (args.includes('--help') || args.includes('-h')) {
76
+ console.log(HELP);
77
+ process.exit(0);
78
+ }
79
+
80
+ if (args.includes('--paths')) {
81
+ const paths = getConfigPaths();
82
+ console.log('\nConfig file locations:\n');
83
+
84
+ if (paths.length === 0) {
85
+ console.log(' No config files found.');
86
+ console.log(`\n Create one with: tl-config --init`);
87
+ } else {
88
+ for (const { type, path } of paths) {
89
+ console.log(` ${type.padEnd(8)} ${path}`);
90
+ }
91
+ }
92
+ console.log();
93
+ process.exit(0);
94
+ }
95
+
96
+ if (args.includes('--init')) {
97
+ const configPath = join(process.cwd(), CONFIG_FILENAME);
98
+
99
+ if (existsSync(configPath)) {
100
+ console.error(`Config already exists: ${configPath}`);
101
+ process.exit(1);
102
+ }
103
+
104
+ writeFileSync(configPath, JSON.stringify(SAMPLE_CONFIG, null, 2) + '\n');
105
+ console.log(`Created: ${configPath}`);
106
+ process.exit(0);
107
+ }
108
+
109
+ if (args.includes('--init-global')) {
110
+ if (existsSync(GLOBAL_CONFIG_PATH)) {
111
+ console.error(`Global config already exists: ${GLOBAL_CONFIG_PATH}`);
112
+ process.exit(1);
113
+ }
114
+
115
+ writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(SAMPLE_CONFIG, null, 2) + '\n');
116
+ console.log(`Created: ${GLOBAL_CONFIG_PATH}`);
117
+ process.exit(0);
118
+ }
119
+
120
+ // Default: show merged config
121
+ const { config, projectRoot } = loadConfig();
122
+ const paths = getConfigPaths();
123
+
124
+ console.log('\nCurrent configuration:\n');
125
+
126
+ if (paths.length > 0) {
127
+ console.log('Loaded from:');
128
+ for (const { type, path } of paths) {
129
+ console.log(` ${type}: ${path}`);
130
+ }
131
+ console.log();
132
+ }
133
+
134
+ console.log(JSON.stringify(config, null, 2));
135
+ console.log();
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * tl-context - Estimate context token usage for files/directories
5
+ *
6
+ * Helps understand what contributes to context usage.
7
+ * Usage: tl-context [path] [--top N]
8
+ */
9
+
10
+ // Prompt info for tl-prompt
11
+ if (process.argv.includes('--prompt')) {
12
+ console.log(JSON.stringify({
13
+ name: 'tl-context',
14
+ desc: 'Estimate token usage for files/directories',
15
+ when: 'before-read',
16
+ example: 'tl-context src/'
17
+ }));
18
+ process.exit(0);
19
+ }
20
+
21
+ import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
22
+ import { join, relative } from 'path';
23
+
24
+ const SKIP_DIRS = new Set([
25
+ 'node_modules', '.git', 'android', 'ios', 'dist', 'build',
26
+ '.expo', '.next', 'coverage', '__pycache__', '.cache'
27
+ ]);
28
+
29
+ const SKIP_EXTENSIONS = new Set([
30
+ '.png', '.jpg', '.jpeg', '.gif', '.ico', '.svg', '.webp',
31
+ '.woff', '.woff2', '.ttf', '.eot',
32
+ '.mp3', '.mp4', '.wav', '.ogg',
33
+ '.zip', '.tar', '.gz',
34
+ '.lock', '.log'
35
+ ]);
36
+
37
+ // Rough token estimate: ~4 chars per token for code
38
+ function estimateTokens(content) {
39
+ return Math.ceil(content.length / 4);
40
+ }
41
+
42
+ function formatTokens(tokens) {
43
+ if (tokens >= 1000000) return `${(tokens / 1000000).toFixed(1)}M`;
44
+ if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}k`;
45
+ return String(tokens);
46
+ }
47
+
48
+ function shouldSkip(name, isDir) {
49
+ if (isDir && SKIP_DIRS.has(name)) return true;
50
+ if (!isDir) {
51
+ const ext = name.substring(name.lastIndexOf('.'));
52
+ if (SKIP_EXTENSIONS.has(ext)) return true;
53
+ }
54
+ return false;
55
+ }
56
+
57
+ function analyzeDir(dirPath, results = [], depth = 0) {
58
+ const entries = readdirSync(dirPath, { withFileTypes: true });
59
+
60
+ for (const entry of entries) {
61
+ if (entry.name.startsWith('.') && entry.name !== '.claude') continue;
62
+ if (shouldSkip(entry.name, entry.isDirectory())) continue;
63
+
64
+ const fullPath = join(dirPath, entry.name);
65
+
66
+ if (entry.isDirectory()) {
67
+ analyzeDir(fullPath, results, depth + 1);
68
+ } else {
69
+ try {
70
+ const content = readFileSync(fullPath, 'utf-8');
71
+ const tokens = estimateTokens(content);
72
+ results.push({ path: fullPath, tokens, lines: content.split('\n').length });
73
+ } catch (e) {
74
+ // Skip binary or unreadable files
75
+ }
76
+ }
77
+ }
78
+
79
+ return results;
80
+ }
81
+
82
+ function printResults(results, rootPath, topN) {
83
+ // Sort by tokens descending
84
+ results.sort((a, b) => b.tokens - a.tokens);
85
+
86
+ const total = results.reduce((sum, r) => sum + r.tokens, 0);
87
+
88
+ console.log(`\n📊 Context Estimate for: ${rootPath}\n`);
89
+ console.log(`Total: ~${formatTokens(total)} tokens across ${results.length} files\n`);
90
+
91
+ if (topN) {
92
+ console.log(`Top ${topN} largest files:\n`);
93
+ results = results.slice(0, topN);
94
+ }
95
+
96
+ const maxPathLen = Math.min(60, Math.max(...results.map(r => relative(rootPath, r.path).length)));
97
+
98
+ console.log(' Tokens Lines Path');
99
+ console.log(' ' + '-'.repeat(maxPathLen + 20));
100
+
101
+ for (const r of results) {
102
+ const relPath = relative(rootPath, r.path);
103
+ const truncPath = relPath.length > 60 ? '...' + relPath.slice(-57) : relPath;
104
+ console.log(` ${formatTokens(r.tokens).padStart(6)} ${String(r.lines).padStart(5)} ${truncPath}`);
105
+ }
106
+
107
+ console.log();
108
+
109
+ // Group by directory
110
+ const byDir = {};
111
+ for (const r of results) {
112
+ const rel = relative(rootPath, r.path);
113
+ const dir = rel.includes('/') ? rel.split('/')[0] : '.';
114
+ byDir[dir] = (byDir[dir] || 0) + r.tokens;
115
+ }
116
+
117
+ const sortedDirs = Object.entries(byDir).sort((a, b) => b[1] - a[1]);
118
+
119
+ console.log('By top-level directory:\n');
120
+ for (const [dir, tokens] of sortedDirs.slice(0, 10)) {
121
+ const pct = ((tokens / total) * 100).toFixed(1);
122
+ console.log(` ${formatTokens(tokens).padStart(6)} ${pct.padStart(5)}% ${dir}/`);
123
+ }
124
+ console.log();
125
+ }
126
+
127
+ // Main
128
+ const args = process.argv.slice(2);
129
+ let targetPath = '.';
130
+ let topN = 20;
131
+
132
+ for (let i = 0; i < args.length; i++) {
133
+ if (args[i] === '--top' && args[i + 1]) {
134
+ topN = parseInt(args[i + 1], 10);
135
+ i++;
136
+ } else if (args[i] === '--all') {
137
+ topN = null;
138
+ } else if (!args[i].startsWith('-')) {
139
+ targetPath = args[i];
140
+ }
141
+ }
142
+
143
+ if (!existsSync(targetPath)) {
144
+ console.error(`Path not found: ${targetPath}`);
145
+ process.exit(1);
146
+ }
147
+
148
+ const stat = statSync(targetPath);
149
+ if (stat.isFile()) {
150
+ const content = readFileSync(targetPath, 'utf-8');
151
+ const tokens = estimateTokens(content);
152
+ console.log(`\n${targetPath}: ~${formatTokens(tokens)} tokens (${content.split('\n').length} lines)\n`);
153
+ } else {
154
+ const results = analyzeDir(targetPath);
155
+ printResults(results, targetPath, topN);
156
+ }