tokenlean 0.1.0 → 0.3.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 +44 -1
- package/bin/tl-cache.mjs +168 -0
- package/bin/tl-component.mjs +109 -83
- package/bin/tl-context.mjs +147 -98
- package/bin/tl-diff.mjs +95 -62
- package/bin/tl-entry.mjs +7 -1
- package/bin/tl-impact.mjs +10 -2
- package/bin/tl-related.mjs +122 -75
- package/bin/tl-search.mjs +148 -27
- package/bin/tl-structure.mjs +152 -83
- package/bin/tl-todo.mjs +8 -1
- package/bin/tl-unused.mjs +29 -19
- package/package.json +2 -1
- package/src/cache.mjs +493 -0
- package/src/config.mjs +6 -0
package/bin/tl-structure.mjs
CHANGED
|
@@ -9,9 +9,26 @@
|
|
|
9
9
|
* Usage: tl-structure [path] [--depth N]
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
// Prompt info for tl-prompt
|
|
13
|
+
if (process.argv.includes('--prompt')) {
|
|
14
|
+
console.log(JSON.stringify({
|
|
15
|
+
name: 'tl-structure',
|
|
16
|
+
desc: 'Project overview with token estimates',
|
|
17
|
+
when: 'before-read',
|
|
18
|
+
example: 'tl-structure'
|
|
19
|
+
}));
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
import { readdirSync, readFileSync, existsSync } from 'fs';
|
|
13
24
|
import { join, basename } from 'path';
|
|
14
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
createOutput,
|
|
27
|
+
parseCommonArgs,
|
|
28
|
+
estimateTokens,
|
|
29
|
+
formatTokens,
|
|
30
|
+
COMMON_OPTIONS_HELP
|
|
31
|
+
} from '../src/output.mjs';
|
|
15
32
|
import {
|
|
16
33
|
getSkipDirs,
|
|
17
34
|
getImportantFiles,
|
|
@@ -19,6 +36,25 @@ import {
|
|
|
19
36
|
} from '../src/project.mjs';
|
|
20
37
|
import { getConfig } from '../src/config.mjs';
|
|
21
38
|
|
|
39
|
+
const HELP = `
|
|
40
|
+
tl-structure - Smart project overview with context estimates
|
|
41
|
+
|
|
42
|
+
Usage: tl-structure [path] [options]
|
|
43
|
+
|
|
44
|
+
Options:
|
|
45
|
+
--depth N, -d N Maximum depth to show (default: 3)
|
|
46
|
+
${COMMON_OPTIONS_HELP}
|
|
47
|
+
|
|
48
|
+
Configure defaults in .tokenleanrc.json:
|
|
49
|
+
"structure": { "depth": 3, "important": ["src", "lib"] }
|
|
50
|
+
|
|
51
|
+
Examples:
|
|
52
|
+
tl-structure # Current directory
|
|
53
|
+
tl-structure src/ -d 2 # Just src, 2 levels deep
|
|
54
|
+
tl-structure -j # JSON output
|
|
55
|
+
tl-structure -q # Quiet (no headers)
|
|
56
|
+
`;
|
|
57
|
+
|
|
22
58
|
function getDirStats(dirPath, skipDirs, importantDirs) {
|
|
23
59
|
let totalTokens = 0;
|
|
24
60
|
let fileCount = 0;
|
|
@@ -38,80 +74,104 @@ function getDirStats(dirPath, skipDirs, importantDirs) {
|
|
|
38
74
|
const content = readFileSync(fullPath, 'utf-8');
|
|
39
75
|
totalTokens += estimateTokens(content);
|
|
40
76
|
fileCount++;
|
|
41
|
-
} catch
|
|
77
|
+
} catch { /* skip binary */ }
|
|
42
78
|
}
|
|
43
79
|
}
|
|
44
|
-
} catch
|
|
80
|
+
} catch { /* permission error */ }
|
|
45
81
|
}
|
|
46
82
|
|
|
47
83
|
walk(dirPath);
|
|
48
84
|
return { totalTokens, fileCount };
|
|
49
85
|
}
|
|
50
86
|
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
87
|
+
function buildTree(dirPath, depth, maxDepth, skipDirs, importantDirs, importantFiles) {
|
|
88
|
+
const tree = [];
|
|
89
|
+
if (depth > maxDepth) return tree;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
93
|
+
|
|
94
|
+
// Sort: directories first, then by importance, then alphabetically
|
|
95
|
+
entries.sort((a, b) => {
|
|
96
|
+
if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
|
|
97
|
+
const aImportant = importantDirs.has(a.name) || importantFiles.has(a.name);
|
|
98
|
+
const bImportant = importantDirs.has(b.name) || importantFiles.has(b.name);
|
|
99
|
+
if (aImportant !== bImportant) return aImportant ? -1 : 1;
|
|
100
|
+
return a.name.localeCompare(b.name);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const filtered = entries.filter(e => {
|
|
104
|
+
if (e.name.startsWith('.') && e.name !== '.claude') return false;
|
|
105
|
+
if (skipDirs.has(e.name)) return false;
|
|
106
|
+
return true;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
for (const entry of filtered) {
|
|
110
|
+
const fullPath = join(dirPath, entry.name);
|
|
111
|
+
const isImportant = importantDirs.has(entry.name) || importantFiles.has(entry.name);
|
|
112
|
+
|
|
113
|
+
if (entry.isDirectory()) {
|
|
114
|
+
const stats = getDirStats(fullPath, skipDirs, importantDirs);
|
|
115
|
+
tree.push({
|
|
116
|
+
name: entry.name,
|
|
117
|
+
type: 'dir',
|
|
118
|
+
important: isImportant,
|
|
119
|
+
fileCount: stats.fileCount,
|
|
120
|
+
tokens: stats.totalTokens,
|
|
121
|
+
children: buildTree(fullPath, depth + 1, maxDepth, skipDirs, importantDirs, importantFiles)
|
|
122
|
+
});
|
|
123
|
+
} else {
|
|
124
|
+
try {
|
|
125
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
126
|
+
const tokens = estimateTokens(content);
|
|
127
|
+
const lines = content.split('\n').length;
|
|
128
|
+
tree.push({
|
|
129
|
+
name: entry.name,
|
|
130
|
+
type: 'file',
|
|
131
|
+
important: isImportant,
|
|
132
|
+
tokens,
|
|
133
|
+
lines
|
|
134
|
+
});
|
|
135
|
+
} catch {
|
|
136
|
+
tree.push({
|
|
137
|
+
name: entry.name,
|
|
138
|
+
type: 'file',
|
|
139
|
+
important: isImportant,
|
|
140
|
+
binary: true
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch { /* permission error */ }
|
|
64
146
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (skipDirs.has(e.name)) return false;
|
|
68
|
-
return true;
|
|
69
|
-
});
|
|
147
|
+
return tree;
|
|
148
|
+
}
|
|
70
149
|
|
|
71
|
-
|
|
72
|
-
|
|
150
|
+
function printTree(tree, out, prefix = '') {
|
|
151
|
+
tree.forEach((entry, index) => {
|
|
152
|
+
const isLast = index === tree.length - 1;
|
|
73
153
|
const connector = isLast ? '└── ' : '├── ';
|
|
74
|
-
const
|
|
154
|
+
const marker = entry.important ? '*' : ' ';
|
|
75
155
|
|
|
76
|
-
|
|
77
|
-
|
|
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)})`
|
|
156
|
+
if (entry.type === 'dir') {
|
|
157
|
+
const sizeInfo = entry.fileCount > 0
|
|
158
|
+
? ` (${entry.fileCount} files, ~${formatTokens(entry.tokens)})`
|
|
83
159
|
: ' (empty)';
|
|
84
|
-
|
|
85
|
-
console.log(`${prefix}${connector}${marker}${entry.name}/${sizeInfo}`);
|
|
160
|
+
out.add(`${prefix}${connector}${marker}${entry.name}/${sizeInfo}`);
|
|
86
161
|
|
|
87
162
|
const newPrefix = prefix + (isLast ? ' ' : '│ ');
|
|
88
|
-
printTree(
|
|
163
|
+
printTree(entry.children, out, newPrefix);
|
|
164
|
+
} else if (entry.binary) {
|
|
165
|
+
out.add(`${prefix}${connector}${marker}${entry.name} (binary)`);
|
|
89
166
|
} else {
|
|
90
|
-
|
|
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
|
-
}
|
|
167
|
+
out.add(`${prefix}${connector}${marker}${entry.name} (~${formatTokens(entry.tokens)}, ${entry.lines}L)`);
|
|
98
168
|
}
|
|
99
169
|
});
|
|
100
170
|
}
|
|
101
171
|
|
|
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
172
|
// Main
|
|
114
173
|
const args = process.argv.slice(2);
|
|
174
|
+
const options = parseCommonArgs(args);
|
|
115
175
|
|
|
116
176
|
// Get config defaults
|
|
117
177
|
const structureConfig = getConfig('structure') || {};
|
|
@@ -119,43 +179,52 @@ const structureConfig = getConfig('structure') || {};
|
|
|
119
179
|
let targetPath = '.';
|
|
120
180
|
let maxDepth = structureConfig.depth || 3;
|
|
121
181
|
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
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);
|
|
182
|
+
// Parse tool-specific options
|
|
183
|
+
for (let i = 0; i < options.remaining.length; i++) {
|
|
184
|
+
const arg = options.remaining[i];
|
|
185
|
+
if ((arg === '--depth' || arg === '-d') && options.remaining[i + 1]) {
|
|
186
|
+
maxDepth = parseInt(options.remaining[i + 1], 10);
|
|
130
187
|
i++;
|
|
131
|
-
} else if (
|
|
132
|
-
|
|
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];
|
|
188
|
+
} else if (!arg.startsWith('-')) {
|
|
189
|
+
targetPath = arg;
|
|
147
190
|
}
|
|
148
191
|
}
|
|
149
192
|
|
|
193
|
+
if (options.help) {
|
|
194
|
+
console.log(HELP);
|
|
195
|
+
process.exit(0);
|
|
196
|
+
}
|
|
197
|
+
|
|
150
198
|
if (!existsSync(targetPath)) {
|
|
151
199
|
console.error(`Path not found: ${targetPath}`);
|
|
152
200
|
process.exit(1);
|
|
153
201
|
}
|
|
154
202
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
203
|
+
// Get combined sets (defaults + user config extensions)
|
|
204
|
+
const skipDirs = getSkipDirs();
|
|
205
|
+
const importantDirs = getImportantDirs();
|
|
206
|
+
const importantFiles = getImportantFiles();
|
|
159
207
|
|
|
160
|
-
|
|
161
|
-
|
|
208
|
+
const out = createOutput(options);
|
|
209
|
+
|
|
210
|
+
const rootStats = getDirStats(targetPath, skipDirs, importantDirs);
|
|
211
|
+
const tree = buildTree(targetPath, 0, maxDepth, skipDirs, importantDirs, importantFiles);
|
|
212
|
+
|
|
213
|
+
// Set JSON data
|
|
214
|
+
out.setData('path', targetPath);
|
|
215
|
+
out.setData('totalFiles', rootStats.fileCount);
|
|
216
|
+
out.setData('totalTokens', rootStats.totalTokens);
|
|
217
|
+
out.setData('depth', maxDepth);
|
|
218
|
+
out.setData('tree', tree);
|
|
219
|
+
|
|
220
|
+
// Headers
|
|
221
|
+
const rootName = targetPath === '.' ? basename(process.cwd()) : targetPath;
|
|
222
|
+
out.header(rootName);
|
|
223
|
+
out.header(`Total: ${rootStats.fileCount} files, ~${formatTokens(rootStats.totalTokens)} tokens`);
|
|
224
|
+
out.header(`(* = important for understanding project)`);
|
|
225
|
+
out.blank();
|
|
226
|
+
|
|
227
|
+
// Tree output
|
|
228
|
+
printTree(tree, out);
|
|
229
|
+
|
|
230
|
+
out.print();
|
package/bin/tl-todo.mjs
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
COMMON_OPTIONS_HELP
|
|
33
33
|
} from '../src/output.mjs';
|
|
34
34
|
import { findProjectRoot, shouldSkip, SKIP_DIRS } from '../src/project.mjs';
|
|
35
|
+
import { withCache } from '../src/cache.mjs';
|
|
35
36
|
|
|
36
37
|
const HELP = `
|
|
37
38
|
tl-todo - Extract TODOs, FIXMEs, and other markers from codebase
|
|
@@ -102,7 +103,13 @@ function findTodos(searchPath, projectRoot) {
|
|
|
102
103
|
// Require : or ( after marker to avoid false positives like "Todo Extraction"
|
|
103
104
|
const commentPattern = `(${commentPrefixes.join('|')})\\s*(${markerPattern})[:(]`;
|
|
104
105
|
const cmd = `rg -n --no-heading -i "${commentPattern}" "${shellEscape(searchPath)}" ${excludes} 2>/dev/null || true`;
|
|
105
|
-
|
|
106
|
+
|
|
107
|
+
const cacheKey = { op: 'rg-todo-markers', pattern: commentPattern, path: searchPath };
|
|
108
|
+
const output = withCache(
|
|
109
|
+
cacheKey,
|
|
110
|
+
() => execSync(cmd, { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 }),
|
|
111
|
+
{ projectRoot }
|
|
112
|
+
);
|
|
106
113
|
|
|
107
114
|
if (!output.trim()) {
|
|
108
115
|
return todos;
|
package/bin/tl-unused.mjs
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
COMMON_OPTIONS_HELP
|
|
30
30
|
} from '../src/output.mjs';
|
|
31
31
|
import { findProjectRoot, shouldSkip, isCodeFile } from '../src/project.mjs';
|
|
32
|
+
import { withCache } from '../src/cache.mjs';
|
|
32
33
|
|
|
33
34
|
const HELP = `
|
|
34
35
|
tl-unused - Find potentially unused exports and unreferenced files
|
|
@@ -221,26 +222,35 @@ function extractImports(content) {
|
|
|
221
222
|
}
|
|
222
223
|
|
|
223
224
|
function findReferencesWithGrep(name, projectRoot, excludeFile) {
|
|
224
|
-
// Use ripgrep for fast reference counting
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
225
|
+
// Use ripgrep for fast reference counting (with caching)
|
|
226
|
+
const cacheKey = { op: 'rg-ref-count', name, types: 'js,ts' };
|
|
227
|
+
|
|
228
|
+
const files = withCache(
|
|
229
|
+
cacheKey,
|
|
230
|
+
() => {
|
|
231
|
+
const args = [
|
|
232
|
+
'-l', // Files only
|
|
233
|
+
'--type', 'js',
|
|
234
|
+
'--type', 'ts',
|
|
235
|
+
'-w', // Word boundary
|
|
236
|
+
name,
|
|
237
|
+
'.'
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
const result = spawnSync('rg', args, {
|
|
241
|
+
cwd: projectRoot,
|
|
242
|
+
encoding: 'utf-8'
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
if (result.error || result.status !== 0) {
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return result.stdout.trim().split('\n').filter(Boolean);
|
|
250
|
+
},
|
|
251
|
+
{ projectRoot }
|
|
252
|
+
);
|
|
242
253
|
|
|
243
|
-
const files = result.stdout.trim().split('\n').filter(Boolean);
|
|
244
254
|
// Exclude the file that exports it
|
|
245
255
|
const relExclude = relative(projectRoot, excludeFile);
|
|
246
256
|
const otherFiles = files.filter(f => f !== relExclude && !f.includes(relExclude));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenlean",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Lean CLI tools for AI agents and developers - reduce context, save tokens",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"bin": {
|
|
23
23
|
"tl-api": "./bin/tl-api.mjs",
|
|
24
24
|
"tl-blame": "./bin/tl-blame.mjs",
|
|
25
|
+
"tl-cache": "./bin/tl-cache.mjs",
|
|
25
26
|
"tl-config": "./bin/tl-config.mjs",
|
|
26
27
|
"tl-context": "./bin/tl-context.mjs",
|
|
27
28
|
"tl-coverage": "./bin/tl-coverage.mjs",
|