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 CHANGED
@@ -18,7 +18,7 @@ the API surface.
18
18
 
19
19
  ## The Solution
20
20
 
21
- tokenlean provides **25 specialized CLI tools** that give you (or your AI agent) exactly the information needed - no
21
+ tokenlean provides **26 specialized CLI tools** that give you (or your AI agent) exactly the information needed - no
22
22
  more, no less. Each tool is designed to answer a specific question about your codebase with minimal token overhead.
23
23
 
24
24
  Instead of reading a 500-line file to understand its exports, run `tl-exports` (~50 tokens). Instead of reading all your
@@ -128,6 +128,7 @@ Search and discover code patterns.
128
128
 
129
129
  | Tool | Description | Example |
130
130
  |-------------|--------------------------------|-----------------------|
131
+ | `tl-cache` | Manage ripgrep result cache | `tl-cache stats` |
131
132
  | `tl-config` | Show/manage configuration | `tl-config --init` |
132
133
  | `tl-prompt` | Generate AI agent instructions | `tl-prompt --minimal` |
133
134
 
@@ -190,6 +191,48 @@ Project config overrides global config. Both are optional.
190
191
 
191
192
  Config values extend built-in defaults (they don't replace them).
192
193
 
194
+ ## Caching
195
+
196
+ tokenlean caches expensive ripgrep operations to speed up repeated searches. The cache uses **git-based invalidation** - it automatically invalidates when you make commits or modify files.
197
+
198
+ ```bash
199
+ tl-cache stats # View cache statistics
200
+ tl-cache clear # Clear cache for current project
201
+ tl-cache clear-all # Clear all cached data
202
+ ```
203
+
204
+ Cache is stored in `~/.tokenlean/cache/` and is enabled by default.
205
+
206
+ ### Cache Configuration
207
+
208
+ ```json
209
+ {
210
+ "cache": {
211
+ "enabled": true,
212
+ "ttl": 300,
213
+ "maxSize": "100MB",
214
+ "location": null
215
+ }
216
+ }
217
+ ```
218
+
219
+ | Option | Default | Description |
220
+ |------------|--------------------|--------------------------------------------|
221
+ | `enabled` | `true` | Enable/disable caching |
222
+ | `ttl` | `300` | Max age in seconds (fallback for non-git) |
223
+ | `maxSize` | `"100MB"` | Max cache size per project |
224
+ | `location` | `null` | Override default `~/.tokenlean/cache` |
225
+
226
+ ### Disable Caching
227
+
228
+ ```bash
229
+ # Disable for a single command
230
+ TOKENLEAN_CACHE=0 tl-search hooks
231
+
232
+ # Disable in config
233
+ echo '{"cache":{"enabled":false}}' > .tokenleanrc.json
234
+ ```
235
+
193
236
  ## Example Workflows
194
237
 
195
238
  ### Starting work on an unfamiliar codebase
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * tl-cache - Manage tokenlean cache
5
+ *
6
+ * View cache statistics and clear cached data.
7
+ *
8
+ * Usage: tl-cache [command]
9
+ */
10
+
11
+ // Prompt info for tl-prompt
12
+ if (process.argv.includes('--prompt')) {
13
+ console.log(JSON.stringify({
14
+ name: 'tl-cache',
15
+ desc: 'Manage tokenlean cache (stats, clear)',
16
+ when: 'maintenance',
17
+ example: 'tl-cache stats'
18
+ }));
19
+ process.exit(0);
20
+ }
21
+
22
+ import {
23
+ createOutput,
24
+ parseCommonArgs,
25
+ COMMON_OPTIONS_HELP
26
+ } from '../src/output.mjs';
27
+ import { findProjectRoot } from '../src/project.mjs';
28
+ import {
29
+ getCacheConfig,
30
+ getCacheStats,
31
+ clearCache,
32
+ getCacheDir
33
+ } from '../src/cache.mjs';
34
+
35
+ const HELP = `
36
+ tl-cache - Manage tokenlean cache
37
+
38
+ Usage: tl-cache <command> [options]
39
+
40
+ Commands:
41
+ stats Show cache statistics (default)
42
+ clear Clear cache for current project
43
+ clear-all Clear cache for all projects
44
+
45
+ ${COMMON_OPTIONS_HELP}
46
+
47
+ Examples:
48
+ tl-cache # Show stats for current project
49
+ tl-cache stats # Same as above
50
+ tl-cache clear # Clear cache for this project
51
+ tl-cache clear-all # Clear all cached data
52
+
53
+ Configuration:
54
+ Cache can be configured in .tokenleanrc.json:
55
+ {
56
+ "cache": {
57
+ "enabled": true, // Enable/disable caching
58
+ "ttl": 300, // Max age in seconds (for non-git repos)
59
+ "maxSize": "100MB", // Max cache size per project
60
+ "location": null // Override ~/.tokenlean/cache
61
+ }
62
+ }
63
+
64
+ Environment:
65
+ TOKENLEAN_CACHE=0 Disable caching for this run
66
+ `;
67
+
68
+ // ─────────────────────────────────────────────────────────────
69
+ // Commands
70
+ // ─────────────────────────────────────────────────────────────
71
+
72
+ function showStats(out, projectRoot) {
73
+ const config = getCacheConfig();
74
+ const projectStats = getCacheStats(projectRoot);
75
+ const globalStats = getCacheStats(null);
76
+
77
+ out.header('Cache Configuration:');
78
+ out.add(` Enabled: ${config.enabled ? 'yes' : 'no'}`);
79
+ out.add(` Location: ${config.location}`);
80
+ out.add(` Max size: ${projectStats.maxSizeFormatted}`);
81
+ out.add(` TTL: ${config.ttl}s (fallback for non-git repos)`);
82
+ out.blank();
83
+
84
+ out.header('Current Project Cache:');
85
+ out.add(` Directory: ${projectStats.location}`);
86
+ out.add(` Entries: ${projectStats.entries}`);
87
+ out.add(` Size: ${projectStats.sizeFormatted}`);
88
+ out.blank();
89
+
90
+ out.header('Global Cache:');
91
+ out.add(` Projects: ${globalStats.projects}`);
92
+ out.add(` Entries: ${globalStats.totalEntries}`);
93
+ out.add(` Total size: ${globalStats.totalSizeFormatted}`);
94
+ out.blank();
95
+
96
+ // Set JSON data
97
+ out.setData('config', config);
98
+ out.setData('project', projectStats);
99
+ out.setData('global', globalStats);
100
+ }
101
+
102
+ function doClear(out, projectRoot) {
103
+ const before = getCacheStats(projectRoot);
104
+ clearCache(projectRoot);
105
+ const after = getCacheStats(projectRoot);
106
+
107
+ out.add(`Cleared ${before.entries} cache entries (${before.sizeFormatted})`);
108
+ out.blank();
109
+
110
+ out.setData('cleared', {
111
+ entries: before.entries,
112
+ size: before.size,
113
+ sizeFormatted: before.sizeFormatted
114
+ });
115
+ }
116
+
117
+ function doClearAll(out) {
118
+ const before = getCacheStats(null);
119
+ clearCache(null);
120
+ const after = getCacheStats(null);
121
+
122
+ out.add(`Cleared ${before.totalEntries} cache entries across ${before.projects} projects (${before.totalSizeFormatted})`);
123
+ out.blank();
124
+
125
+ out.setData('cleared', {
126
+ projects: before.projects,
127
+ entries: before.totalEntries,
128
+ size: before.totalSize,
129
+ sizeFormatted: before.totalSizeFormatted
130
+ });
131
+ }
132
+
133
+ // ─────────────────────────────────────────────────────────────
134
+ // Main
135
+ // ─────────────────────────────────────────────────────────────
136
+
137
+ const args = process.argv.slice(2);
138
+ const options = parseCommonArgs(args);
139
+
140
+ if (options.help) {
141
+ console.log(HELP);
142
+ process.exit(0);
143
+ }
144
+
145
+ const command = options.remaining.find(a => !a.startsWith('-')) || 'stats';
146
+ const projectRoot = findProjectRoot();
147
+ const out = createOutput(options);
148
+
149
+ switch (command) {
150
+ case 'stats':
151
+ showStats(out, projectRoot);
152
+ break;
153
+
154
+ case 'clear':
155
+ doClear(out, projectRoot);
156
+ break;
157
+
158
+ case 'clear-all':
159
+ doClearAll(out);
160
+ break;
161
+
162
+ default:
163
+ console.error(`Unknown command: ${command}`);
164
+ console.log('Use: stats, clear, or clear-all');
165
+ process.exit(1);
166
+ }
167
+
168
+ out.print();
@@ -21,16 +21,27 @@ if (process.argv.includes('--prompt')) {
21
21
  }
22
22
 
23
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
- }
24
+ import { join, relative } from 'path';
25
+ import {
26
+ createOutput,
27
+ parseCommonArgs,
28
+ estimateTokens,
29
+ formatTokens,
30
+ COMMON_OPTIONS_HELP
31
+ } from '../src/output.mjs';
32
+ import { findProjectRoot } from '../src/project.mjs';
33
+
34
+ const HELP = `
35
+ tl-component - React component analyzer
36
+
37
+ Usage: tl-component <file.tsx> [options]
38
+ ${COMMON_OPTIONS_HELP}
39
+
40
+ Examples:
41
+ tl-component src/Button.tsx # Analyze component
42
+ tl-component src/App.tsx -j # JSON output
43
+ tl-component src/Modal.tsx -q # Quiet (minimal)
44
+ `;
34
45
 
35
46
  function extractImports(content) {
36
47
  const imports = {
@@ -175,79 +186,14 @@ function extractRedux(content) {
175
186
  return redux;
176
187
  }
177
188
 
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
189
  // Main
244
190
  const args = process.argv.slice(2);
245
- const targetFile = args[0];
191
+ const options = parseCommonArgs(args);
192
+ const targetFile = options.remaining.find(a => !a.startsWith('-'));
246
193
 
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);
194
+ if (options.help || !targetFile) {
195
+ console.log(HELP);
196
+ process.exit(options.help ? 0 : 1);
251
197
  }
252
198
 
253
199
  const fullPath = targetFile.startsWith('/') ? targetFile : join(process.cwd(), targetFile);
@@ -258,11 +204,12 @@ if (!existsSync(fullPath)) {
258
204
 
259
205
  const content = readFileSync(fullPath, 'utf-8');
260
206
  const projectRoot = findProjectRoot();
207
+ const relPath = relative(projectRoot, fullPath);
261
208
 
262
209
  const analysis = {
263
- file: relative(projectRoot, fullPath),
210
+ file: relPath,
264
211
  lines: content.split('\n').length,
265
- tokens: Math.ceil(content.length / 4),
212
+ tokens: estimateTokens(content),
266
213
  imports: extractImports(content),
267
214
  hooks: extractHooks(content),
268
215
  propsInfo: extractProps(content),
@@ -271,4 +218,83 @@ const analysis = {
271
218
  redux: extractRedux(content)
272
219
  };
273
220
 
274
- printAnalysis(analysis);
221
+ const out = createOutput(options);
222
+
223
+ // Set JSON data
224
+ out.setData('file', analysis.file);
225
+ out.setData('lines', analysis.lines);
226
+ out.setData('tokens', analysis.tokens);
227
+ out.setData('components', analysis.components);
228
+ out.setData('props', analysis.propsInfo);
229
+ out.setData('hooks', analysis.hooks);
230
+ out.setData('imports', analysis.imports);
231
+ out.setData('styles', analysis.styles);
232
+ out.setData('redux', analysis.redux);
233
+
234
+ // Headers
235
+ out.header(`Component Analysis: ${analysis.file}`);
236
+ out.header(`${analysis.lines} lines, ~${formatTokens(analysis.tokens)} tokens`);
237
+ out.blank();
238
+
239
+ // Components
240
+ if (analysis.components.length > 0) {
241
+ out.add(`Components: ${analysis.components.join(', ')}`);
242
+ }
243
+
244
+ // Props
245
+ if (analysis.propsInfo) {
246
+ out.blank();
247
+ out.add(`${analysis.propsInfo.name}:`);
248
+ for (const p of analysis.propsInfo.props) {
249
+ const opt = p.optional ? '?' : '';
250
+ out.add(` ${p.name}${opt}: ${p.type}`);
251
+ }
252
+ }
253
+
254
+ // Hooks
255
+ if (analysis.hooks.length > 0) {
256
+ out.blank();
257
+ out.add(`Hooks: ${analysis.hooks.join(', ')}`);
258
+ }
259
+
260
+ // Redux
261
+ if (analysis.redux.dispatch || analysis.redux.selectors.length > 0) {
262
+ out.blank();
263
+ out.add('Redux:');
264
+ if (analysis.redux.selectors.length > 0) {
265
+ out.add(` Selectors: ${analysis.redux.selectors.join(', ')}`);
266
+ }
267
+ if (analysis.redux.actions.length > 0) {
268
+ out.add(` Actions: ${analysis.redux.actions.join(', ')}`);
269
+ }
270
+ }
271
+
272
+ // Imports summary
273
+ out.blank();
274
+ out.add('Imports:');
275
+ if (analysis.imports.react.length > 0) {
276
+ out.add(` React: ${analysis.imports.react.join(', ')}`);
277
+ }
278
+ if (analysis.imports.reactNative.length > 0) {
279
+ out.add(` React Native: ${analysis.imports.reactNative.join(', ')}`);
280
+ }
281
+ if (analysis.imports.internal.length > 0) {
282
+ out.add(` Internal: ${analysis.imports.internal.length} modules`);
283
+ for (const i of analysis.imports.internal.slice(0, 5)) {
284
+ out.add(` ${i.source}`);
285
+ }
286
+ if (analysis.imports.internal.length > 5) {
287
+ out.add(` ... and ${analysis.imports.internal.length - 5} more`);
288
+ }
289
+ }
290
+ if (analysis.imports.external.length > 0) {
291
+ out.add(` External: ${analysis.imports.external.map(i => i.source).join(', ')}`);
292
+ }
293
+
294
+ // Styles
295
+ if (analysis.styles.length > 0) {
296
+ out.blank();
297
+ out.add(`Styling: ${analysis.styles.join(', ')}`);
298
+ }
299
+
300
+ out.print();