llmview 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/README.md CHANGED
@@ -20,9 +20,9 @@ Create any number of view files in your project. These can be saved anywhere. Fo
20
20
  new_feature.llmview
21
21
  ```
22
22
 
23
- And use them selectively:
23
+ And use one:
24
24
 
25
- ```
25
+ ```bash
26
26
  llmview .views/backend.llmview
27
27
  ```
28
28
 
@@ -96,24 +96,30 @@ my_project/
96
96
 
97
97
  The `-n` argument includes line numbers in each file, similar to `cat -n`. This uses more tokens, but can also be useful context.
98
98
 
99
- ## Integrating with your tools
99
+ ### Only list selected files
100
+
101
+ The `-l` argument lets you use selected files for something else besides rendering the context to stdout. For example, to create a zip of selected files.
102
+
103
+ ```bash
104
+ llmview .views/backend.llmview -l | zip context.zip -@
105
+ ```
100
106
 
101
- For agentic tools, you can ask an agent to use the `llmview` command itself with views you defined. This might save it some time getting up to speed on your project, especially across sessions.
107
+ ### Using `llmview` as a filter
102
108
 
103
- For interactive use such as code reviews, you can pipe the results to an API or just copy the contents to your clipboard and paste into a LLM web app, like:
109
+ Instead of reading from a view file, you can use it as a filter. For example, to render all the unstaged changes in your repo:
104
110
 
105
111
  ```bash
106
- llmview .views/backend.llmview | pbcopy
112
+ git diff --name-only | llmview -
107
113
  ```
108
114
 
109
115
  ## Renderers
110
116
 
111
- This tool comes with a set of opinionated file renderers. They are informed by the file extension. Exceptions are made for:
117
+ This tool comes with a set of opinionated file renderers based on the file extension. Currently they are:
112
118
 
113
119
  - CSV (truncated by default, preserving the header and the first 10 lines)
114
120
  - Excel, media files, and other non-text formats (omitted)
115
121
 
116
- There is also a max size of 250KB per file. If a file is larger than that, it is not rendered.
122
+ There is also a max size of 250KB per file. If a code file is larger than that, it is not rendered. (If a CSV file is larger, it's still rendered and just truncated as usual.)
117
123
 
118
124
  ## Dry run to get statistics
119
125
 
package/dist/build.js CHANGED
@@ -34,24 +34,19 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.buildDirectory = void 0;
37
- const os = __importStar(require("os"));
38
- const fs = __importStar(require("fs"));
37
+ const fs = __importStar(require("fs/promises"));
39
38
  const path = __importStar(require("path"));
40
- const git_1 = require("./git");
39
+ const ignore_1 = require("./ignore");
41
40
  const constants_1 = require("./constants");
42
- const expandUser = (inputPath) => {
43
- return inputPath.replace(/^~/, os.homedir());
44
- };
45
- const buildDirectory = (projectPath) => {
46
- projectPath = expandUser(projectPath);
41
+ const buildDirectory = async (projectPath) => {
47
42
  const ignores = [
48
- { ig: (0, git_1.createIgnore)(constants_1.BASE_IGNORE_CONTENT), scope: '' },
43
+ { ig: (0, ignore_1.createIgnore)(constants_1.BASE_IGNORE_CONTENT), scope: '' },
49
44
  ];
50
- const rootGitignore = (0, git_1.createIgnoreFromFile)(path.join(projectPath, '.gitignore'));
45
+ const rootGitignore = await (0, ignore_1.createIgnoreFromFile)(path.join(projectPath, '.gitignore'));
51
46
  if (rootGitignore) {
52
47
  ignores.push({ ig: rootGitignore, scope: '' });
53
48
  }
54
- const stat = fs.statSync(projectPath);
49
+ const stat = await fs.stat(projectPath);
55
50
  const rootNode = {
56
51
  ino: stat.ino,
57
52
  name: path.basename(projectPath),
@@ -62,17 +57,17 @@ const buildDirectory = (projectPath) => {
62
57
  rootPath: projectPath,
63
58
  children: [],
64
59
  };
65
- rootNode.children = buildChildrenNodes(projectPath, rootNode, ignores);
60
+ rootNode.children = await buildChildrenNodes(projectPath, rootNode, ignores);
66
61
  return rootNode;
67
62
  };
68
63
  exports.buildDirectory = buildDirectory;
69
- const buildChildrenNodes = (rootPath, parentNode, ignores) => {
64
+ const buildChildrenNodes = async (rootPath, parentNode, ignores) => {
70
65
  const currentPath = path.join(rootPath, parentNode.relativePath);
71
- const entries = fs.readdirSync(currentPath);
66
+ const entries = await fs.readdir(currentPath);
72
67
  const nodes = [];
73
68
  for (const entry of entries) {
74
69
  const entryFullPath = path.join(currentPath, entry);
75
- const lstat = fs.lstatSync(entryFullPath);
70
+ const lstat = await fs.lstat(entryFullPath);
76
71
  if (lstat.isSymbolicLink()) {
77
72
  continue;
78
73
  }
@@ -80,7 +75,7 @@ const buildChildrenNodes = (rootPath, parentNode, ignores) => {
80
75
  const nodeRelativePath = parentNode.relativePath
81
76
  ? `${parentNode.relativePath}/${entry}`
82
77
  : entry;
83
- if ((0, git_1.isPathIgnored)(nodeRelativePath, isDirectory, ignores)) {
78
+ if ((0, ignore_1.isPathIgnored)(nodeRelativePath, isDirectory, ignores)) {
84
79
  continue;
85
80
  }
86
81
  const nodeBase = {
@@ -96,17 +91,18 @@ const buildChildrenNodes = (rootPath, parentNode, ignores) => {
96
91
  type: 'directory',
97
92
  children: [],
98
93
  };
99
- const nestedIg = (0, git_1.createIgnoreFromFile)(path.join(entryFullPath, '.gitignore'));
94
+ const nestedIg = await (0, ignore_1.createIgnoreFromFile)(path.join(entryFullPath, '.gitignore'));
100
95
  const childIgnores = nestedIg
101
96
  ? [...ignores, { ig: nestedIg, scope: nodeRelativePath }]
102
97
  : ignores;
103
- dirNode.children = buildChildrenNodes(rootPath, dirNode, childIgnores);
98
+ dirNode.children = await buildChildrenNodes(rootPath, dirNode, childIgnores);
104
99
  nodes.push(dirNode);
105
100
  }
106
101
  else {
107
102
  const fileNode = {
108
103
  ...nodeBase,
109
104
  type: 'file',
105
+ size: lstat.size,
110
106
  };
111
107
  nodes.push(fileNode);
112
108
  }
package/dist/cli.js CHANGED
@@ -34,13 +34,43 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  };
35
35
  })();
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
- const fs = __importStar(require("fs"));
37
+ const fs = __importStar(require("fs/promises"));
38
38
  const path = __importStar(require("path"));
39
39
  const build_1 = require("./build");
40
40
  const render_1 = require("./render");
41
41
  const select_1 = require("./select");
42
- const parseLlmviewFile = (filePath) => {
43
- const content = fs.readFileSync(filePath, 'utf-8');
42
+ const constants_1 = require("./constants");
43
+ const readStdin = () => {
44
+ return new Promise((resolve, reject) => {
45
+ const chunks = [];
46
+ process.stdin.on('data', (chunk) => chunks.push(chunk));
47
+ process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
48
+ process.stdin.on('error', reject);
49
+ });
50
+ };
51
+ const readPatterns = async (pathArg) => {
52
+ let content;
53
+ if (!pathArg || pathArg === '-') {
54
+ if (process.stdin.isTTY) {
55
+ console.error('Error: No input file specified and stdin is a terminal');
56
+ console.error('Usage: llmview <view-file> or pipe patterns via stdin');
57
+ process.exit(1);
58
+ }
59
+ content = await readStdin();
60
+ }
61
+ else {
62
+ const resolvedPath = path.resolve(pathArg);
63
+ try {
64
+ content = await fs.readFile(resolvedPath, 'utf-8');
65
+ }
66
+ catch (err) {
67
+ if (err.code === 'ENOENT') {
68
+ console.error(`File not found: ${resolvedPath}`);
69
+ process.exit(1);
70
+ }
71
+ throw err;
72
+ }
73
+ }
44
74
  return content
45
75
  .split('\n')
46
76
  .map((line) => line.trim())
@@ -51,15 +81,16 @@ const HELP_TEXT = `llmview - Generate LLM context from codebases using gitignore
51
81
  Usage: llmview [options] <view-file>
52
82
 
53
83
  Arguments:
54
- <view-file> Path to a .llmview file containing glob patterns
84
+ <view-file> Path to a .llmview file, or - to read patterns from stdin
55
85
 
56
86
  Options:
87
+ -l, --list List selected files only (no content)
57
88
  -n, --number Include line numbers in output
58
89
  -t, --tree Include directory tree of selected files
59
90
  -v, --verbose Print file statistics to stderr
60
91
  -h, --help Show this help message
61
92
  `;
62
- const main = () => {
93
+ const main = async () => {
63
94
  const args = process.argv.slice(2);
64
95
  if (args.includes('-h') || args.includes('--help')) {
65
96
  console.log(HELP_TEXT);
@@ -68,37 +99,36 @@ const main = () => {
68
99
  const verbose = args.includes('-v') || args.includes('--verbose');
69
100
  const includeTree = args.includes('-t') || args.includes('--tree');
70
101
  const lineNumbers = args.includes('-n') || args.includes('--number');
71
- const llmviewPath = args.find((arg) => !arg.startsWith('-'));
72
- if (!llmviewPath) {
73
- console.log(HELP_TEXT);
74
- process.exit(1);
75
- }
76
- const resolvedPath = path.resolve(llmviewPath);
77
- if (!fs.existsSync(resolvedPath)) {
78
- console.error(`File not found: ${resolvedPath}`);
79
- process.exit(1);
80
- }
81
- const patterns = parseLlmviewFile(resolvedPath);
102
+ const listOnly = args.includes('-l') || args.includes('--list');
103
+ const positionalArgs = args.filter((arg) => !arg.startsWith('-'));
104
+ const patterns = await readPatterns(positionalArgs[0]);
82
105
  if (patterns.length === 0) {
83
- console.error('No patterns found in llmview file');
106
+ console.error('No patterns found in input');
84
107
  process.exit(1);
85
108
  }
86
- const rootNode = (0, build_1.buildDirectory)(process.cwd());
109
+ const rootNode = await (0, build_1.buildDirectory)(process.cwd());
87
110
  const { selectedFiles, visiblePaths } = (0, select_1.selectFiles)(rootNode, patterns, {
88
111
  verbose,
89
112
  });
113
+ if (listOnly) {
114
+ selectedFiles.forEach((file) => console.log(file.relativePath));
115
+ process.exit(0);
116
+ }
90
117
  let output = '';
91
118
  if (includeTree) {
92
- output += (0, render_1.renderHierarchy)(rootNode, { verbose }, visiblePaths);
119
+ output += (0, render_1.renderDirectory)(rootNode, { verbose }, visiblePaths);
93
120
  }
94
- output += (0, render_1.renderFiles)(rootNode.rootPath, selectedFiles, {
121
+ output += await (0, render_1.renderFiles)(rootNode.rootPath, selectedFiles, {
95
122
  verbose,
96
123
  lineNumbers,
97
124
  });
98
- const estimatedTokens = Math.ceil(output.length / 4);
125
+ const estimatedTokens = Math.ceil(output.length / constants_1.CHARS_PER_TOKEN_ESTIMATE);
99
126
  if (verbose) {
100
127
  console.warn(`Estimated tokens: ${estimatedTokens}`);
101
128
  }
102
129
  console.log(output);
103
130
  };
104
- main();
131
+ main().catch((err) => {
132
+ console.error(err);
133
+ process.exit(1);
134
+ });
package/dist/constants.js CHANGED
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MAX_FILE_SIZE_KB = exports.BASE_IGNORE_CONTENT = void 0;
3
+ exports.CHARS_PER_TOKEN_ESTIMATE = exports.CSV_PREVIEW_LINES = exports.MAX_FILE_SIZE_KB = exports.BASE_IGNORE_CONTENT = void 0;
4
4
  exports.BASE_IGNORE_CONTENT = `.git
5
5
  .DS_Store
6
6
  __pycache__/
7
7
  node_modules/`;
8
8
  exports.MAX_FILE_SIZE_KB = 250;
9
+ exports.CSV_PREVIEW_LINES = 10;
10
+ exports.CHARS_PER_TOKEN_ESTIMATE = 4;
@@ -37,13 +37,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.isPathIgnored = exports.createIgnore = exports.createIgnoreFromFile = void 0;
40
- const fs = __importStar(require("fs"));
40
+ const fs = __importStar(require("fs/promises"));
41
41
  const ignore_1 = __importDefault(require("ignore"));
42
- const createIgnoreFromFile = (gitignorePath) => {
43
- if (!fs.existsSync(gitignorePath)) {
42
+ const createIgnoreFromFile = async (gitignorePath) => {
43
+ try {
44
+ const content = await fs.readFile(gitignorePath, 'utf8');
45
+ return (0, ignore_1.default)().add(content);
46
+ }
47
+ catch {
44
48
  return undefined;
45
49
  }
46
- return (0, ignore_1.default)().add(fs.readFileSync(gitignorePath, 'utf8'));
47
50
  };
48
51
  exports.createIgnoreFromFile = createIgnoreFromFile;
49
52
  const createIgnore = (content) => {
@@ -35,47 +35,28 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.RENDER_RULES = exports.defaultRenderer = void 0;
37
37
  const fs = __importStar(require("fs"));
38
+ const fsPromises = __importStar(require("fs/promises"));
38
39
  const path = __importStar(require("path"));
40
+ const readline = __importStar(require("readline"));
39
41
  const constants_1 = require("./constants");
40
- const readFirstNLines = (filePath, n) => {
41
- const fd = fs.openSync(filePath, 'r');
42
- const bufferSize = 64 * 1024;
43
- const buffer = Buffer.alloc(bufferSize);
42
+ const readFirstNLines = async (filePath, n) => {
44
43
  const lines = [];
45
- let leftover = '';
46
- let hasMore = false;
44
+ const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
45
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
47
46
  try {
48
- while (lines.length < n) {
49
- const bytesRead = fs.readSync(fd, buffer, 0, bufferSize, null);
50
- if (bytesRead === 0)
51
- break; // EOF
52
- const chunk = leftover + buffer.toString('utf-8', 0, bytesRead);
53
- const chunkLines = chunk.split('\n');
54
- leftover = chunkLines.pop() || '';
55
- for (const line of chunkLines) {
56
- if (lines.length < n) {
57
- lines.push(line);
58
- }
59
- else {
60
- hasMore = true;
61
- break;
62
- }
63
- }
64
- if (hasMore)
65
- break;
66
- }
67
- if (!hasMore && lines.length >= n) {
68
- if (leftover !== '') {
69
- hasMore = true;
47
+ for await (const line of rl) {
48
+ if (lines.length < n) {
49
+ lines.push(line);
70
50
  }
71
51
  else {
72
- hasMore = fs.readSync(fd, buffer, 0, 1, null) > 0;
52
+ return { lines, hasMore: true };
73
53
  }
74
54
  }
75
- return { lines, hasMore };
55
+ return { lines, hasMore: false };
76
56
  }
77
57
  finally {
78
- fs.closeSync(fd);
58
+ rl.close();
59
+ stream.destroy();
79
60
  }
80
61
  };
81
62
  const numberLines = (lines) => {
@@ -86,13 +67,12 @@ const numberLines = (lines) => {
86
67
  })
87
68
  .join('\n');
88
69
  };
89
- const defaultRenderer = (rootPath, file, options) => {
70
+ const defaultRenderer = async (rootPath, file, options) => {
90
71
  const fullPath = path.join(rootPath, file.relativePath);
91
- const stats = fs.statSync(fullPath);
92
- if (stats.size > constants_1.MAX_FILE_SIZE_KB * 1024) {
93
- return `(File contents excluded: size ${(stats.size / 1024).toFixed(2)}KB exceeds ${constants_1.MAX_FILE_SIZE_KB}KB limit)`;
72
+ if (file.size > constants_1.MAX_FILE_SIZE_KB * 1024) {
73
+ return `(File contents excluded: size ${(file.size / 1024).toFixed(2)}KB exceeds ${constants_1.MAX_FILE_SIZE_KB}KB limit)`;
94
74
  }
95
- const content = fs.readFileSync(fullPath, 'utf-8');
75
+ const content = await fsPromises.readFile(fullPath, 'utf-8');
96
76
  if (options.lineNumbers) {
97
77
  const lines = content.split('\n');
98
78
  return numberLines(lines);
@@ -104,9 +84,9 @@ exports.RENDER_RULES = [
104
84
  // csv
105
85
  {
106
86
  matcher: (name) => ['.csv'].some((ext) => name.toLowerCase().endsWith(ext)),
107
- renderer: (rootPath, file, options) => {
87
+ renderer: async (rootPath, file, options) => {
108
88
  const fullPath = path.join(rootPath, file.relativePath);
109
- const { lines, hasMore } = readFirstNLines(fullPath, 10);
89
+ const { lines, hasMore } = await readFirstNLines(fullPath, constants_1.CSV_PREVIEW_LINES);
110
90
  const preview = options.lineNumbers
111
91
  ? numberLines(lines)
112
92
  : lines.join('\n');
@@ -116,16 +96,16 @@ exports.RENDER_RULES = [
116
96
  // excel
117
97
  {
118
98
  matcher: (name) => ['.xls', '.xlsx'].some((ext) => name.toLowerCase().endsWith(ext)),
119
- renderer: (rootPath, file, options) => '(Contents excluded)',
99
+ renderer: async () => '(Contents excluded)',
120
100
  },
121
101
  // media
122
102
  {
123
103
  matcher: (name) => ['.ico', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.mp4'].some((ext) => name.toLowerCase().endsWith(ext)),
124
- renderer: (rootPath, file, options) => '(Contents excluded)',
104
+ renderer: async () => '(Contents excluded)',
125
105
  },
126
106
  // misc
127
107
  {
128
108
  matcher: (name) => ['.pdf', '.zip'].some((ext) => name.toLowerCase().endsWith(ext)),
129
- renderer: (rootPath, file, options) => '(Contents excluded)',
109
+ renderer: async () => '(Contents excluded)',
130
110
  },
131
111
  ];
package/dist/render.js CHANGED
@@ -1,48 +1,37 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.renderFiles = exports.renderHierarchy = void 0;
4
- const renderers_1 = require("./renderers");
5
- const renderHierarchy = (node, options, visiblePaths, currentDepth = 0) => {
6
- if (visiblePaths &&
7
- node.relativePath &&
8
- !visiblePaths.has(node.relativePath)) {
9
- return null;
10
- }
3
+ exports.renderFiles = exports.renderDirectory = void 0;
4
+ const render_rules_1 = require("./render-rules");
5
+ const renderDirectory = (rootNode, options, visiblePaths) => {
6
+ return renderDirectoryNode(rootNode, options, visiblePaths, 0);
7
+ };
8
+ exports.renderDirectory = renderDirectory;
9
+ const renderDirectoryNode = (node, options, visiblePaths, currentDepth) => {
11
10
  const { indentChar = ' ' } = options;
12
11
  const indent = indentChar.repeat(currentDepth);
13
12
  if (node.type === 'file') {
14
13
  return `${indent}${node.name}`;
15
14
  }
16
- let result = `${indent}${node.name}/`;
17
- if (node.children.length === 0) {
18
- return result;
19
- }
20
15
  const childrenOutput = node.children
21
- .map((child) => (0, exports.renderHierarchy)(child, options, visiblePaths, currentDepth + 1))
22
- .filter((output) => output !== null)
16
+ .filter((child) => !visiblePaths ||
17
+ !child.relativePath ||
18
+ visiblePaths.has(child.relativePath))
19
+ .map((child) => renderDirectoryNode(child, options, visiblePaths, currentDepth + 1))
23
20
  .join('\n');
21
+ const result = `${indent}${node.name}/`;
24
22
  if (currentDepth === 0) {
25
- return `\`\`\`
26
- <directory>
27
- ${result}
28
- ${childrenOutput}
29
- </directory>
30
- \`\`\`\n\n`;
31
- }
32
- else {
33
- return `${result}\n${childrenOutput}`;
23
+ return `\`\`\`\n<directory>\n${result}\n${childrenOutput}\n</directory>\n\`\`\`\n\n`;
34
24
  }
25
+ return childrenOutput ? `${result}\n${childrenOutput}` : result;
35
26
  };
36
- exports.renderHierarchy = renderHierarchy;
37
- const renderFiles = (rootPath, files, options) => {
38
- return files
39
- .map((file) => renderFileBlock(rootPath, file, options))
40
- .join('\n\n');
27
+ const renderFiles = async (rootPath, files, options) => {
28
+ const renderedBlocks = await Promise.all(files.map((file) => renderFile(rootPath, file, options)));
29
+ return renderedBlocks.join('\n\n');
41
30
  };
42
31
  exports.renderFiles = renderFiles;
43
- const renderFileBlock = (rootPath, file, options) => {
32
+ const renderFile = async (rootPath, file, options) => {
44
33
  const renderer = getRenderer(file);
45
- const rendered = renderer(rootPath, file, options);
34
+ const rendered = await renderer(rootPath, file, options);
46
35
  if (options.verbose) {
47
36
  console.warn({ path: file.relativePath, length: rendered.length });
48
37
  }
@@ -53,10 +42,10 @@ ${rendered}
53
42
  \`\`\``;
54
43
  };
55
44
  const getRenderer = (file) => {
56
- for (const { matcher, renderer } of renderers_1.RENDER_RULES) {
45
+ for (const { matcher, renderer } of render_rules_1.RENDER_RULES) {
57
46
  if (matcher(file.name)) {
58
47
  return renderer;
59
48
  }
60
49
  }
61
- return renderers_1.defaultRenderer;
50
+ return render_rules_1.defaultRenderer;
62
51
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llmview",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "bin": {
5
5
  "llmview": "dist/cli.js"
6
6
  },
@@ -22,10 +22,7 @@
22
22
  "ts-node": "^10.9.2",
23
23
  "typescript": "^5.8.2"
24
24
  },
25
- "files": [
26
- "dist",
27
- "README.md"
28
- ],
25
+ "files": ["dist", "README.md"],
29
26
  "repository": {
30
27
  "type": "git",
31
28
  "url": "git+https://github.com/noahtren/llmview.git"