tokenlean 0.1.0 → 0.2.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/bin/tl-search.mjs CHANGED
@@ -22,7 +22,35 @@ if (process.argv.includes('--prompt')) {
22
22
  }
23
23
 
24
24
  import { spawn, execSync } from 'child_process';
25
- import { loadConfig, getSearchPatterns, CONFIG_FILENAME } from '../src/config.mjs';
25
+ import {
26
+ createOutput,
27
+ parseCommonArgs,
28
+ COMMON_OPTIONS_HELP
29
+ } from '../src/output.mjs';
30
+ import { loadConfig, CONFIG_FILENAME } from '../src/config.mjs';
31
+
32
+ const HELP = `
33
+ tl-search - Run pre-defined search patterns
34
+
35
+ Usage: tl-search <pattern-name> [options]
36
+ ${COMMON_OPTIONS_HELP}
37
+
38
+ Configure patterns in ${CONFIG_FILENAME}:
39
+ {
40
+ "searchPatterns": {
41
+ "hooks": {
42
+ "description": "Find lifecycle hooks",
43
+ "pattern": "use(Effect|State|Callback)",
44
+ "glob": "**/*.{ts,tsx}"
45
+ }
46
+ }
47
+ }
48
+
49
+ Examples:
50
+ tl-search # List available patterns
51
+ tl-search hooks # Run the "hooks" pattern
52
+ tl-search todos -j # JSON output
53
+ `;
26
54
 
27
55
  // Check for ripgrep
28
56
  try {
@@ -32,13 +60,14 @@ try {
32
60
  process.exit(1);
33
61
  }
34
62
 
35
- function showHelp(patterns) {
63
+ function showPatterns(patterns, out) {
36
64
  const names = Object.keys(patterns);
37
65
 
38
66
  if (names.length === 0) {
39
- console.log('\nNo search patterns defined.');
40
- console.log(`\nAdd patterns to ${CONFIG_FILENAME}:`);
41
- console.log(`
67
+ out.header('No search patterns defined.');
68
+ out.blank();
69
+ out.add(`Add patterns to ${CONFIG_FILENAME}:`);
70
+ out.add(`
42
71
  {
43
72
  "searchPatterns": {
44
73
  "hooks": {
@@ -48,25 +77,37 @@ function showHelp(patterns) {
48
77
  }
49
78
  }
50
79
  }`);
51
- process.exit(0);
80
+ return;
52
81
  }
53
82
 
54
- console.log('\nAvailable search patterns:\n');
83
+ out.header('Available search patterns:');
84
+ out.blank();
55
85
 
56
86
  const maxLen = Math.max(...names.map(k => k.length));
57
87
 
88
+ // For JSON output, set data
89
+ out.setData('patterns', Object.entries(patterns).map(([name, config]) => ({
90
+ name,
91
+ description: config.description || '',
92
+ pattern: config.pattern,
93
+ glob: config.glob
94
+ })));
95
+
58
96
  for (const [name, config] of Object.entries(patterns)) {
59
97
  const paddedName = name.padEnd(maxLen);
60
- console.log(` ${paddedName} ${config.description || '(no description)'}`);
98
+ out.add(` ${paddedName} ${config.description || '(no description)'}`);
61
99
  }
62
100
 
63
- console.log('\nUsage: tl-search <pattern-name>');
64
- console.log('Example: tl-search hooks\n');
101
+ out.blank();
102
+ out.add('Usage: tl-search <pattern-name>');
103
+ out.add('Example: tl-search hooks');
65
104
  }
66
105
 
67
- function runSearch(name, config, rootDir) {
68
- console.log(`\nSearching: ${config.description || name}`);
69
- console.log(`Pattern: ${config.pattern}\n`);
106
+ function runSearch(name, config, rootDir, jsonMode) {
107
+ if (!jsonMode) {
108
+ console.log(`\nSearching: ${config.description || name}`);
109
+ console.log(`Pattern: ${config.pattern}\n`);
110
+ }
70
111
 
71
112
  if (config.type === 'glob-only') {
72
113
  const args = ['--files', '-g', config.pattern];
@@ -76,12 +117,39 @@ function runSearch(name, config, rootDir) {
76
117
  }
77
118
  }
78
119
  args.push(rootDir);
79
- const proc = spawn('rg', args, { stdio: 'inherit' });
80
- proc.on('close', code => process.exit(code === 1 ? 0 : code));
120
+
121
+ if (jsonMode) {
122
+ // Capture output for JSON
123
+ try {
124
+ const result = execSync(`rg ${args.map(a => `"${a}"`).join(' ')}`, {
125
+ encoding: 'utf-8',
126
+ cwd: rootDir
127
+ });
128
+ const files = result.trim().split('\n').filter(Boolean);
129
+ console.log(JSON.stringify({
130
+ pattern: name,
131
+ description: config.description,
132
+ type: 'glob-only',
133
+ files,
134
+ count: files.length
135
+ }, null, 2));
136
+ } catch {
137
+ console.log(JSON.stringify({
138
+ pattern: name,
139
+ files: [],
140
+ count: 0
141
+ }, null, 2));
142
+ }
143
+ } else {
144
+ const proc = spawn('rg', args, { stdio: 'inherit' });
145
+ proc.on('close', code => process.exit(code === 1 ? 0 : code));
146
+ }
81
147
  return;
82
148
  }
83
149
 
84
- const args = ['--color=always', '-n', '-e', config.pattern];
150
+ const args = jsonMode
151
+ ? ['-n', '--json', '-e', config.pattern]
152
+ : ['--color=always', '-n', '-e', config.pattern];
85
153
 
86
154
  if (config.glob) {
87
155
  args.push('-g', config.glob);
@@ -95,29 +163,76 @@ function runSearch(name, config, rootDir) {
95
163
 
96
164
  args.push(rootDir);
97
165
 
98
- const proc = spawn('rg', args, { stdio: 'inherit' });
99
- proc.on('close', code => {
100
- if (code === 1) {
101
- console.log('No matches found.');
166
+ if (jsonMode) {
167
+ try {
168
+ const result = execSync(`rg ${args.map(a => `"${a}"`).join(' ')} 2>/dev/null || true`, {
169
+ encoding: 'utf-8',
170
+ maxBuffer: 10 * 1024 * 1024
171
+ });
172
+ const matches = result.trim().split('\n')
173
+ .filter(line => line.startsWith('{'))
174
+ .map(line => {
175
+ try { return JSON.parse(line); } catch { return null; }
176
+ })
177
+ .filter(Boolean)
178
+ .filter(m => m.type === 'match')
179
+ .map(m => ({
180
+ file: m.data.path.text,
181
+ line: m.data.line_number,
182
+ text: m.data.lines.text.trim()
183
+ }));
184
+
185
+ console.log(JSON.stringify({
186
+ pattern: name,
187
+ description: config.description,
188
+ searchPattern: config.pattern,
189
+ matches,
190
+ count: matches.length
191
+ }, null, 2));
192
+ } catch {
193
+ console.log(JSON.stringify({
194
+ pattern: name,
195
+ matches: [],
196
+ count: 0
197
+ }, null, 2));
102
198
  }
103
- process.exit(code === 1 ? 0 : code);
104
- });
199
+ } else {
200
+ const proc = spawn('rg', args, { stdio: 'inherit' });
201
+ proc.on('close', code => {
202
+ if (code === 1) {
203
+ console.log('No matches found.');
204
+ }
205
+ process.exit(code === 1 ? 0 : code);
206
+ });
207
+ }
105
208
  }
106
209
 
107
210
  // Main
211
+ const args = process.argv.slice(2);
212
+ const options = parseCommonArgs(args);
213
+
214
+ if (options.help) {
215
+ console.log(HELP);
216
+ process.exit(0);
217
+ }
218
+
108
219
  const { config, projectRoot } = loadConfig();
109
220
  const patterns = config.searchPatterns || {};
110
- const patternName = process.argv[2];
221
+ const patternName = options.remaining.find(a => !a.startsWith('-'));
111
222
 
112
- if (!patternName || patternName === '--help' || patternName === '-h') {
113
- showHelp(patterns);
223
+ if (!patternName) {
224
+ const out = createOutput(options);
225
+ showPatterns(patterns, out);
226
+ out.print();
114
227
  process.exit(0);
115
228
  }
116
229
 
117
230
  if (!patterns[patternName]) {
118
231
  console.error(`\nUnknown pattern: "${patternName}"`);
119
- showHelp(patterns);
232
+ const out = createOutput({ ...options, json: false });
233
+ showPatterns(patterns, out);
234
+ out.print();
120
235
  process.exit(1);
121
236
  }
122
237
 
123
- runSearch(patternName, patterns[patternName], projectRoot);
238
+ runSearch(patternName, patterns[patternName], projectRoot, options.json);
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenlean",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Lean CLI tools for AI agents and developers - reduce context, save tokens",
5
5
  "type": "module",
6
6
  "engines": {