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.
- package/README.md +248 -0
- package/bin/tl-api.mjs +515 -0
- package/bin/tl-blame.mjs +345 -0
- package/bin/tl-complexity.mjs +514 -0
- package/bin/tl-component.mjs +274 -0
- package/bin/tl-config.mjs +135 -0
- package/bin/tl-context.mjs +156 -0
- package/bin/tl-coverage.mjs +456 -0
- package/bin/tl-deps.mjs +474 -0
- package/bin/tl-diff.mjs +183 -0
- package/bin/tl-entry.mjs +256 -0
- package/bin/tl-env.mjs +376 -0
- package/bin/tl-exports.mjs +583 -0
- package/bin/tl-flow.mjs +324 -0
- package/bin/tl-history.mjs +289 -0
- package/bin/tl-hotspots.mjs +321 -0
- package/bin/tl-impact.mjs +345 -0
- package/bin/tl-prompt.mjs +175 -0
- package/bin/tl-related.mjs +227 -0
- package/bin/tl-routes.mjs +627 -0
- package/bin/tl-search.mjs +123 -0
- package/bin/tl-structure.mjs +161 -0
- package/bin/tl-symbols.mjs +430 -0
- package/bin/tl-todo.mjs +341 -0
- package/bin/tl-types.mjs +441 -0
- package/bin/tl-unused.mjs +494 -0
- package/package.json +55 -0
- package/src/config.mjs +271 -0
- package/src/output.mjs +251 -0
- package/src/project.mjs +277 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* tl-prompt - Output AI agent instructions for tokenlean tools
|
|
5
|
+
*
|
|
6
|
+
* Dynamically discovers all tl-* commands and generates instructions
|
|
7
|
+
* by calling each with --prompt flag.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* tl-prompt # Full instructions (markdown)
|
|
11
|
+
* tl-prompt --minimal # Compact version
|
|
12
|
+
* tl-prompt --list # Simple list
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { execSync } from 'child_process';
|
|
16
|
+
import { readdirSync } from 'fs';
|
|
17
|
+
import { dirname, join } from 'path';
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
19
|
+
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
|
|
22
|
+
// Prompt info for tl-prompt (meta!)
|
|
23
|
+
if (process.argv.includes('--prompt')) {
|
|
24
|
+
process.exit(0); // No output - this IS the prompt tool
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Discover all tl-* tools and get their prompt info
|
|
29
|
+
*/
|
|
30
|
+
function discoverTools() {
|
|
31
|
+
const tools = [];
|
|
32
|
+
const binDir = __dirname;
|
|
33
|
+
|
|
34
|
+
const files = readdirSync(binDir)
|
|
35
|
+
.filter(f => f.startsWith('tl-') && f.endsWith('.mjs') && f !== 'tl-prompt.mjs');
|
|
36
|
+
|
|
37
|
+
for (const file of files) {
|
|
38
|
+
try {
|
|
39
|
+
const result = execSync(`node "${join(binDir, file)}" --prompt`, {
|
|
40
|
+
encoding: 'utf-8',
|
|
41
|
+
timeout: 5000,
|
|
42
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
43
|
+
}).trim();
|
|
44
|
+
|
|
45
|
+
if (result) {
|
|
46
|
+
const info = JSON.parse(result);
|
|
47
|
+
tools.push(info);
|
|
48
|
+
}
|
|
49
|
+
} catch (e) {
|
|
50
|
+
// Tool doesn't support --prompt or errored - skip it
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return tools;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Group tools by their 'when' category
|
|
59
|
+
*/
|
|
60
|
+
function groupTools(tools) {
|
|
61
|
+
const groups = {
|
|
62
|
+
'before-read': { title: 'Before Reading Files', tools: [] },
|
|
63
|
+
'before-modify': { title: 'Before Modifying Files', tools: [] },
|
|
64
|
+
'search': { title: 'Searching', tools: [] }
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
for (const tool of tools) {
|
|
68
|
+
const group = groups[tool.when] || groups['search'];
|
|
69
|
+
group.tools.push(tool);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return groups;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function generateFull(tools) {
|
|
76
|
+
const groups = groupTools(tools);
|
|
77
|
+
|
|
78
|
+
let output = `## tokenlean CLI Tools
|
|
79
|
+
|
|
80
|
+
Use these tools to explore the codebase efficiently and save context tokens.
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
for (const [key, group] of Object.entries(groups)) {
|
|
84
|
+
if (group.tools.length === 0) continue;
|
|
85
|
+
|
|
86
|
+
output += `
|
|
87
|
+
### ${group.title}
|
|
88
|
+
|
|
89
|
+
| Command | Purpose |
|
|
90
|
+
|---------|---------|
|
|
91
|
+
`;
|
|
92
|
+
for (const tool of group.tools) {
|
|
93
|
+
output += `| \`${tool.name}\` | ${tool.desc} |\n`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
output += `
|
|
98
|
+
### Tips
|
|
99
|
+
|
|
100
|
+
- Prefer \`tl-symbols\` over reading entire files when you only need signatures
|
|
101
|
+
- Use \`tl-impact\` before refactoring to understand dependencies
|
|
102
|
+
- Check \`tl-context\` to avoid reading unnecessarily large files
|
|
103
|
+
- All tools support \`--help\` for more options
|
|
104
|
+
`;
|
|
105
|
+
|
|
106
|
+
return output;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function generateMinimal(tools) {
|
|
110
|
+
const groups = groupTools(tools);
|
|
111
|
+
|
|
112
|
+
let lines = ['## tokenlean Tools', ''];
|
|
113
|
+
|
|
114
|
+
for (const [key, group] of Object.entries(groups)) {
|
|
115
|
+
if (group.tools.length === 0) continue;
|
|
116
|
+
|
|
117
|
+
const names = group.tools.map(t => `\`${t.name}\``).join(', ');
|
|
118
|
+
const label = group.title.replace(' Files', '').toLowerCase();
|
|
119
|
+
lines.push(`${label}: ${names}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
lines.push('');
|
|
123
|
+
lines.push('Prefer `tl-symbols` over reading full files. Use `--help` on any command.');
|
|
124
|
+
|
|
125
|
+
return lines.join('\n');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function generateList(tools) {
|
|
129
|
+
return tools.map(t => `${t.name}: ${t.desc}`).join('\n');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Parse args
|
|
133
|
+
const args = process.argv.slice(2);
|
|
134
|
+
|
|
135
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
136
|
+
console.log(`
|
|
137
|
+
tl-prompt - Output AI agent instructions for tokenlean tools
|
|
138
|
+
|
|
139
|
+
Usage:
|
|
140
|
+
tl-prompt Full markdown instructions
|
|
141
|
+
tl-prompt --minimal Compact version (fewer tokens)
|
|
142
|
+
tl-prompt --list Simple list of tools
|
|
143
|
+
tl-prompt --json Raw JSON data
|
|
144
|
+
tl-prompt --help Show this help
|
|
145
|
+
|
|
146
|
+
Integration examples:
|
|
147
|
+
# Add to CLAUDE.md
|
|
148
|
+
tl-prompt >> CLAUDE.md
|
|
149
|
+
|
|
150
|
+
# Add to .cursorrules
|
|
151
|
+
tl-prompt --minimal >> .cursorrules
|
|
152
|
+
|
|
153
|
+
# Use in a hook (regenerate on session start)
|
|
154
|
+
tl-prompt > .ai-tools.md
|
|
155
|
+
`);
|
|
156
|
+
process.exit(0);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Discover tools dynamically
|
|
160
|
+
const tools = discoverTools();
|
|
161
|
+
|
|
162
|
+
if (tools.length === 0) {
|
|
163
|
+
console.error('No tools found. Make sure tl-* commands are in the same directory.');
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (args.includes('--json')) {
|
|
168
|
+
console.log(JSON.stringify(tools, null, 2));
|
|
169
|
+
} else if (args.includes('--minimal') || args.includes('-m')) {
|
|
170
|
+
console.log(generateMinimal(tools));
|
|
171
|
+
} else if (args.includes('--list') || args.includes('-l')) {
|
|
172
|
+
console.log(generateList(tools));
|
|
173
|
+
} else {
|
|
174
|
+
console.log(generateFull(tools));
|
|
175
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* tl-related - Find related files (tests, types, usages)
|
|
5
|
+
*
|
|
6
|
+
* Given a file, finds its tests, type definitions, and files that import it.
|
|
7
|
+
* Helps understand what to read before modifying a file.
|
|
8
|
+
*
|
|
9
|
+
* Usage: tl-related <file>
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Prompt info for tl-prompt
|
|
13
|
+
if (process.argv.includes('--prompt')) {
|
|
14
|
+
console.log(JSON.stringify({
|
|
15
|
+
name: 'tl-related',
|
|
16
|
+
desc: 'Find tests, types, and importers of a file',
|
|
17
|
+
when: 'before-modify',
|
|
18
|
+
example: 'tl-related src/Button.tsx'
|
|
19
|
+
}));
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
24
|
+
import { join, dirname, basename, relative, extname } from 'path';
|
|
25
|
+
import { execSync } from 'child_process';
|
|
26
|
+
import { shellEscape } from '../src/output.mjs';
|
|
27
|
+
|
|
28
|
+
try { execSync('which rg', { stdio: 'ignore' }); } catch {
|
|
29
|
+
console.error('โ ๏ธ ripgrep (rg) not found. Install: brew install ripgrep');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const SKIP_DIRS = new Set([
|
|
34
|
+
'node_modules', '.git', 'android', 'ios', 'dist', 'build', '.expo', '.next'
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
function findProjectRoot() {
|
|
38
|
+
let dir = process.cwd();
|
|
39
|
+
while (dir !== '/') {
|
|
40
|
+
if (existsSync(join(dir, 'package.json'))) return dir;
|
|
41
|
+
dir = dirname(dir);
|
|
42
|
+
}
|
|
43
|
+
return process.cwd();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function estimateTokens(filePath) {
|
|
47
|
+
try {
|
|
48
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
49
|
+
return Math.ceil(content.length / 4);
|
|
50
|
+
} catch {
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function formatTokens(tokens) {
|
|
56
|
+
if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}k`;
|
|
57
|
+
return String(tokens);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function findTestFiles(filePath, projectRoot) {
|
|
61
|
+
const dir = dirname(filePath);
|
|
62
|
+
const name = basename(filePath, extname(filePath));
|
|
63
|
+
const tests = [];
|
|
64
|
+
|
|
65
|
+
// Common test patterns
|
|
66
|
+
const patterns = [
|
|
67
|
+
join(dir, `${name}.test.ts`),
|
|
68
|
+
join(dir, `${name}.test.tsx`),
|
|
69
|
+
join(dir, `${name}.spec.ts`),
|
|
70
|
+
join(dir, `${name}.spec.tsx`),
|
|
71
|
+
join(dir, '__tests__', `${name}.test.ts`),
|
|
72
|
+
join(dir, '__tests__', `${name}.test.tsx`),
|
|
73
|
+
join(dir, '__tests__', `${name}.spec.ts`),
|
|
74
|
+
join(dir, '__tests__', `${name}.spec.tsx`),
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
for (const p of patterns) {
|
|
78
|
+
if (existsSync(p)) {
|
|
79
|
+
tests.push(p);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return tests;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function findTypeFiles(filePath, projectRoot) {
|
|
87
|
+
const dir = dirname(filePath);
|
|
88
|
+
const name = basename(filePath, extname(filePath));
|
|
89
|
+
const types = [];
|
|
90
|
+
|
|
91
|
+
// Check for adjacent type file
|
|
92
|
+
const typePatterns = [
|
|
93
|
+
join(dir, `${name}.types.ts`),
|
|
94
|
+
join(dir, 'types.ts'),
|
|
95
|
+
join(dir, 'types', `${name}.ts`),
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
for (const p of typePatterns) {
|
|
99
|
+
if (existsSync(p)) {
|
|
100
|
+
types.push(p);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check project-wide types directory
|
|
105
|
+
const globalTypes = join(projectRoot, 'src', 'types');
|
|
106
|
+
if (existsSync(globalTypes)) {
|
|
107
|
+
const typeFiles = readdirSync(globalTypes).filter(f => f.endsWith('.ts'));
|
|
108
|
+
for (const tf of typeFiles.slice(0, 5)) {
|
|
109
|
+
types.push(join(globalTypes, tf));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return types;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function findImporters(filePath, projectRoot) {
|
|
117
|
+
const name = basename(filePath, extname(filePath));
|
|
118
|
+
|
|
119
|
+
const importers = new Set();
|
|
120
|
+
|
|
121
|
+
// Search for files that might import this module
|
|
122
|
+
try {
|
|
123
|
+
const result = execSync(
|
|
124
|
+
`rg -l -g "*.{js,mjs,ts,tsx,jsx}" -e "${shellEscape(name)}" "${shellEscape(projectRoot)}" 2>/dev/null || true`,
|
|
125
|
+
{ encoding: 'utf-8', maxBuffer: 5 * 1024 * 1024 }
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
for (const line of result.trim().split('\n')) {
|
|
129
|
+
if (!line) continue;
|
|
130
|
+
if (line === filePath) continue;
|
|
131
|
+
if (line.includes('.test.') || line.includes('.spec.')) continue;
|
|
132
|
+
if (line.includes('node_modules')) continue;
|
|
133
|
+
|
|
134
|
+
// Verify it's actually an import statement
|
|
135
|
+
try {
|
|
136
|
+
const content = readFileSync(line, 'utf-8');
|
|
137
|
+
// Match: from '...name' or from "...name" or require('...name')
|
|
138
|
+
const pattern = new RegExp(`(?:from|require)\\s*\\(?\\s*['"][^'"]*\\/${name}(?:\\.m?[jt]sx?)?['"]`);
|
|
139
|
+
if (pattern.test(content)) {
|
|
140
|
+
importers.add(line);
|
|
141
|
+
}
|
|
142
|
+
} catch { /* skip unreadable files */ }
|
|
143
|
+
}
|
|
144
|
+
} catch (e) { /* rg not found or no matches */ }
|
|
145
|
+
|
|
146
|
+
return Array.from(importers);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function findSiblings(filePath) {
|
|
150
|
+
const dir = dirname(filePath);
|
|
151
|
+
const siblings = [];
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const files = readdirSync(dir).filter(f => {
|
|
155
|
+
const fullPath = join(dir, f);
|
|
156
|
+
return statSync(fullPath).isFile() &&
|
|
157
|
+
f !== basename(filePath) &&
|
|
158
|
+
!f.includes('.test.') &&
|
|
159
|
+
!f.includes('.spec.') &&
|
|
160
|
+
(f.endsWith('.ts') || f.endsWith('.tsx'));
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
for (const f of files.slice(0, 5)) {
|
|
164
|
+
siblings.push(join(dir, f));
|
|
165
|
+
}
|
|
166
|
+
} catch (e) { /* permission error */ }
|
|
167
|
+
|
|
168
|
+
return siblings;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function printSection(title, files, projectRoot) {
|
|
172
|
+
if (files.length === 0) return;
|
|
173
|
+
|
|
174
|
+
console.log(`\n${title}`);
|
|
175
|
+
for (const f of files) {
|
|
176
|
+
const rel = relative(projectRoot, f);
|
|
177
|
+
const tokens = estimateTokens(f);
|
|
178
|
+
console.log(` ${rel} (~${formatTokens(tokens)})`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Main
|
|
183
|
+
const args = process.argv.slice(2);
|
|
184
|
+
const targetFile = args[0];
|
|
185
|
+
|
|
186
|
+
if (!targetFile) {
|
|
187
|
+
console.log('\nUsage: claude-related <file>\n');
|
|
188
|
+
console.log('Finds tests, types, and importers for a given file.');
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const fullPath = join(process.cwd(), targetFile);
|
|
193
|
+
if (!existsSync(fullPath)) {
|
|
194
|
+
console.error(`File not found: ${targetFile}`);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const projectRoot = findProjectRoot();
|
|
199
|
+
const relPath = relative(projectRoot, fullPath);
|
|
200
|
+
|
|
201
|
+
console.log(`\n๐ Related files for: ${relPath}`);
|
|
202
|
+
|
|
203
|
+
const tests = findTestFiles(fullPath, projectRoot);
|
|
204
|
+
const types = findTypeFiles(fullPath, projectRoot);
|
|
205
|
+
const importers = findImporters(fullPath, projectRoot);
|
|
206
|
+
const siblings = findSiblings(fullPath);
|
|
207
|
+
|
|
208
|
+
printSection('๐งช Tests', tests, projectRoot);
|
|
209
|
+
printSection('๐ Types', types, projectRoot);
|
|
210
|
+
printSection('๐ฅ Imported by', importers.slice(0, 10), projectRoot);
|
|
211
|
+
printSection('๐ฅ Siblings', siblings, projectRoot);
|
|
212
|
+
|
|
213
|
+
const totalFiles = tests.length + types.length + Math.min(importers.length, 10) + siblings.length;
|
|
214
|
+
if (totalFiles === 0) {
|
|
215
|
+
console.log('\n No related files found.');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Summary
|
|
219
|
+
const totalTokens = [...tests, ...types, ...importers.slice(0, 10), ...siblings]
|
|
220
|
+
.reduce((sum, f) => sum + estimateTokens(f), 0);
|
|
221
|
+
|
|
222
|
+
console.log(`\n๐ Total: ${totalFiles} related files, ~${formatTokens(totalTokens)} tokens`);
|
|
223
|
+
|
|
224
|
+
if (importers.length > 10) {
|
|
225
|
+
console.log(` (${importers.length - 10} more importers not shown)`);
|
|
226
|
+
}
|
|
227
|
+
console.log();
|