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.
@@ -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 { estimateTokens, formatTokens } from '../src/output.mjs';
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 (e) { /* skip binary */ }
77
+ } catch { /* skip binary */ }
42
78
  }
43
79
  }
44
- } catch (e) { /* permission error */ }
80
+ } catch { /* permission error */ }
45
81
  }
46
82
 
47
83
  walk(dirPath);
48
84
  return { totalTokens, fileCount };
49
85
  }
50
86
 
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
- });
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
- 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
- });
147
+ return tree;
148
+ }
70
149
 
71
- filtered.forEach((entry, index) => {
72
- const isLast = index === filtered.length - 1;
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 fullPath = join(dirPath, entry.name);
154
+ const marker = entry.important ? '*' : ' ';
75
155
 
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)})`
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(fullPath, newPrefix, depth + 1, maxDepth, skipDirs, importantDirs, importantFiles);
163
+ printTree(entry.children, out, newPrefix);
164
+ } else if (entry.binary) {
165
+ out.add(`${prefix}${connector}${marker}${entry.name} (binary)`);
89
166
  } 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
- }
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
- // 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);
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 (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];
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
- 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`);
203
+ // Get combined sets (defaults + user config extensions)
204
+ const skipDirs = getSkipDirs();
205
+ const importantDirs = getImportantDirs();
206
+ const importantFiles = getImportantFiles();
159
207
 
160
- printTree(targetPath, '', 0, maxDepth, skipDirs, importantDirs, importantFiles);
161
- console.log();
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
- const output = execSync(cmd, { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 });
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 args = [
226
- '-l', // Files only
227
- '--type', 'js',
228
- '--type', 'ts',
229
- '-w', // Word boundary
230
- name,
231
- '.'
232
- ];
233
-
234
- const result = spawnSync('rg', args, {
235
- cwd: projectRoot,
236
- encoding: 'utf-8'
237
- });
238
-
239
- if (result.error || result.status !== 0) {
240
- return 0;
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.1.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",