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.
@@ -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();