@skroyc/librarian 0.1.0 → 0.2.1

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.
Files changed (76) hide show
  1. package/README.md +4 -16
  2. package/dist/agents/context-schema.d.ts +1 -1
  3. package/dist/agents/context-schema.d.ts.map +1 -1
  4. package/dist/agents/context-schema.js +5 -2
  5. package/dist/agents/context-schema.js.map +1 -1
  6. package/dist/agents/react-agent.d.ts.map +1 -1
  7. package/dist/agents/react-agent.js +63 -170
  8. package/dist/agents/react-agent.js.map +1 -1
  9. package/dist/agents/tool-runtime.d.ts.map +1 -1
  10. package/dist/cli.d.ts +1 -1
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +53 -49
  13. package/dist/cli.js.map +1 -1
  14. package/dist/config.d.ts +1 -1
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/config.js +115 -69
  17. package/dist/config.js.map +1 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +246 -150
  21. package/dist/index.js.map +1 -1
  22. package/dist/tools/file-finding.tool.d.ts +1 -1
  23. package/dist/tools/file-finding.tool.d.ts.map +1 -1
  24. package/dist/tools/file-finding.tool.js +70 -130
  25. package/dist/tools/file-finding.tool.js.map +1 -1
  26. package/dist/tools/file-listing.tool.d.ts +7 -1
  27. package/dist/tools/file-listing.tool.d.ts.map +1 -1
  28. package/dist/tools/file-listing.tool.js +96 -80
  29. package/dist/tools/file-listing.tool.js.map +1 -1
  30. package/dist/tools/file-reading.tool.d.ts +4 -1
  31. package/dist/tools/file-reading.tool.d.ts.map +1 -1
  32. package/dist/tools/file-reading.tool.js +107 -45
  33. package/dist/tools/file-reading.tool.js.map +1 -1
  34. package/dist/tools/grep-content.tool.d.ts +13 -1
  35. package/dist/tools/grep-content.tool.d.ts.map +1 -1
  36. package/dist/tools/grep-content.tool.js +186 -144
  37. package/dist/tools/grep-content.tool.js.map +1 -1
  38. package/dist/utils/error-utils.d.ts +9 -0
  39. package/dist/utils/error-utils.d.ts.map +1 -0
  40. package/dist/utils/error-utils.js +61 -0
  41. package/dist/utils/error-utils.js.map +1 -0
  42. package/dist/utils/file-utils.d.ts +1 -0
  43. package/dist/utils/file-utils.d.ts.map +1 -1
  44. package/dist/utils/file-utils.js +81 -9
  45. package/dist/utils/file-utils.js.map +1 -1
  46. package/dist/utils/format-utils.d.ts +25 -0
  47. package/dist/utils/format-utils.d.ts.map +1 -0
  48. package/dist/utils/format-utils.js +111 -0
  49. package/dist/utils/format-utils.js.map +1 -0
  50. package/dist/utils/gitignore-service.d.ts +10 -0
  51. package/dist/utils/gitignore-service.d.ts.map +1 -0
  52. package/dist/utils/gitignore-service.js +91 -0
  53. package/dist/utils/gitignore-service.js.map +1 -0
  54. package/dist/utils/logger.d.ts +2 -2
  55. package/dist/utils/logger.d.ts.map +1 -1
  56. package/dist/utils/logger.js +35 -34
  57. package/dist/utils/logger.js.map +1 -1
  58. package/dist/utils/path-utils.js +3 -3
  59. package/dist/utils/path-utils.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/agents/context-schema.ts +5 -2
  62. package/src/agents/react-agent.ts +694 -784
  63. package/src/agents/tool-runtime.ts +4 -4
  64. package/src/cli.ts +95 -57
  65. package/src/config.ts +192 -90
  66. package/src/index.ts +402 -180
  67. package/src/tools/file-finding.tool.ts +198 -310
  68. package/src/tools/file-listing.tool.ts +245 -202
  69. package/src/tools/file-reading.tool.ts +225 -138
  70. package/src/tools/grep-content.tool.ts +387 -307
  71. package/src/utils/error-utils.ts +95 -0
  72. package/src/utils/file-utils.ts +104 -19
  73. package/src/utils/format-utils.ts +190 -0
  74. package/src/utils/gitignore-service.ts +123 -0
  75. package/src/utils/logger.ts +112 -77
  76. package/src/utils/path-utils.ts +3 -3
@@ -1,277 +1,167 @@
1
- import { tool } from "langchain";
2
- import { z } from "zod";
3
1
  import fs from "node:fs/promises";
4
- import type { Dirent } from "node:fs";
5
2
  import path from "node:path";
3
+ import { Glob } from "bun";
4
+ import { tool } from "langchain";
5
+ import { z } from "zod";
6
+ import { formatToolError, getToolSuggestion } from "../utils/error-utils.js";
7
+ import { GitIgnoreService } from "../utils/gitignore-service.js";
6
8
  import { logger } from "../utils/logger.js";
7
9
 
8
- // Find files matching glob patterns in a directory
9
- async function findFiles(
10
- searchPath: string,
11
- patterns: string[],
12
- options: {
13
- exclude?: string[];
14
- recursive?: boolean;
15
- maxResults?: number;
16
- includeHidden?: boolean;
17
- } = {},
18
- ): Promise<string[]> {
19
- const {
20
- exclude = [],
21
- recursive = true,
22
- maxResults = 1000,
23
- includeHidden = false,
24
- } = options;
25
-
26
- const foundFiles: string[] = [];
27
- const processedPaths = new Set<string>();
28
-
29
- // Process each pattern
30
- for (const pattern of patterns) {
31
- // Only add **/ prefix if recursive and pattern doesn't already contain path separators
32
- const effectivePattern =
33
- recursive && !pattern.includes("**") && !pattern.includes("/")
34
- ? `**/${pattern}`
35
- : pattern;
36
-
37
- // Simple glob matching implementation
38
- const matchingFiles = await simpleGlobSearch(searchPath, effectivePattern, {
39
- recursive,
40
- includeHidden,
41
- });
42
-
43
- for (const file of matchingFiles) {
44
- if (foundFiles.length >= maxResults) {
45
- break;
46
- }
47
-
48
- if (processedPaths.has(file)) {
49
- continue;
50
- }
51
-
52
- processedPaths.add(file);
53
-
54
- // Check if file should be excluded
55
- const relativePath = path.relative(searchPath, file);
56
- if (exclude.some((excl) => simpleMatch(relativePath, excl))) {
57
- continue;
58
- }
59
-
60
- foundFiles.push(file);
61
- }
62
-
63
- if (foundFiles.length >= maxResults) {
64
- break;
65
- }
66
- }
67
-
68
- return foundFiles.slice(0, maxResults);
69
- }
70
-
71
- async function handleDirectoryEntry(
72
- entry: Dirent,
73
- basePath: string,
74
- pattern: string,
75
- options: { recursive: boolean; includeHidden: boolean },
76
- results: string[],
77
- ): Promise<void> {
78
- const fullPath = path.join(basePath, entry.name);
79
-
80
- if (pattern === "**" || pattern.includes("**") || options.recursive) {
81
- const basePattern = pattern.split("/")[0] || pattern;
82
- if (simpleMatch(entry.name, basePattern)) {
83
- results.push(fullPath);
84
- }
85
-
86
- if (options.recursive) {
87
- const subDirResults = await simpleGlobSearch(
88
- fullPath,
89
- pattern,
90
- options,
91
- );
92
- results.push(...subDirResults);
93
- }
94
- }
95
- }
96
-
97
- function handleFileEntry(
98
- entry: Dirent,
99
- basePath: string,
100
- pattern: string,
101
- results: string[],
102
- ): void {
103
- const fullPath = path.join(basePath, entry.name);
104
- const relativePath = path.relative(basePath, fullPath);
105
- const basePattern = pattern.includes("**/")
106
- ? pattern.split("**/")[1] || ""
107
- : pattern;
108
- if (
109
- simpleMatch(entry.name, basePattern) ||
110
- simpleMatch(relativePath, basePattern)
111
- ) {
112
- results.push(fullPath);
113
- }
114
- }
115
-
116
- // Simple glob search implementation (basic pattern matching)
117
- async function simpleGlobSearch(
118
- basePath: string,
119
- pattern: string,
120
- options: {
121
- recursive: boolean;
122
- includeHidden: boolean;
123
- },
124
- ): Promise<string[]> {
125
- const results: string[] = [];
126
-
127
- try {
128
- const entries = await fs.readdir(basePath, { withFileTypes: true });
129
-
130
- for (const entry of entries) {
131
- if (!options.includeHidden && entry.name.startsWith(".")) {
132
- continue;
133
- }
134
-
135
- if (entry.isDirectory()) {
136
- await handleDirectoryEntry(entry, basePath, pattern, options, results);
137
- } else if (entry.isFile()) {
138
- handleFileEntry(entry, basePath, pattern, results);
139
- }
140
- }
141
- } catch {
142
- return [];
143
- }
144
-
145
- return results;
146
- }
147
-
148
- // Simple pattern matching (supports * and ? wildcards)
149
- function simpleMatch(str: string, pattern: string): boolean {
150
- // Handle exact match first
151
- if (pattern === str) {
152
- return true;
153
- }
154
-
155
- // Convert glob pattern to regex
156
- const regexPattern = pattern
157
- .replace(/\./g, "\\.") // Escape dots
158
- .replace(/\*/g, ".*") // Replace * with .*
159
- .replace(/\?/g, "."); // Replace ? with .
160
-
161
- const regex = new RegExp(`^${regexPattern}$`);
162
- return regex.test(str);
163
- }
164
-
165
10
  // Create the modernized tool using the tool() function
166
- export const fileFindTool = tool(
167
- async (
168
- {
169
- searchPath = ".",
170
- patterns,
171
- exclude = [],
172
- recursive = true,
173
- maxResults = 1000,
174
- includeHidden = false,
175
- },
176
- config,
177
- ) => {
178
- const timingId = logger.timingStart("fileFind");
179
-
180
- logger.info("TOOL", "file_find called", {
181
- searchPath,
182
- patterns,
183
- exclude,
184
- recursive,
185
- maxResults,
186
- includeHidden,
187
- });
188
-
189
- try {
190
- // Get working directory from config context - required for security
191
- const workingDir = config?.context?.workingDir;
192
- if (!workingDir) {
193
- throw new Error(
194
- "Context with workingDir is required for file operations",
195
- );
196
- }
197
- logger.debug("TOOL", "Working directory", {
198
- workingDir: workingDir.replace(Bun.env.HOME || "", "~"),
199
- });
200
-
201
- // Validate the path to prevent directory traversal
202
- const resolvedPath = path.resolve(workingDir, searchPath);
203
- const resolvedWorkingDir = path.resolve(workingDir);
204
- const relativePath = path.relative(resolvedWorkingDir, resolvedPath);
205
-
206
- logger.debug("TOOL", "Path validation", {
207
- resolvedPath: resolvedPath.replace(Bun.env.HOME || "", "~"),
208
- resolvedWorkingDir: resolvedWorkingDir.replace(Bun.env.HOME || "", "~"),
209
- relativePath,
210
- validated: !relativePath.startsWith(".."),
211
- });
212
-
213
- if (relativePath.startsWith("..")) {
214
- logger.error(
215
- "PATH",
216
- "Search path escapes working directory sandbox",
217
- undefined,
218
- { searchPath, relativePath },
219
- );
220
- throw new Error(
221
- `Search path "${searchPath}" attempts to escape the working directory sandbox`,
222
- );
223
- }
224
-
225
- // Validate that the search path exists and is a directory
226
- const stats = await fs.stat(resolvedPath);
227
- if (!stats.isDirectory()) {
228
- logger.error("TOOL", "Search path is not a directory", undefined, {
229
- searchPath,
230
- });
231
- throw new Error(`Search path "${searchPath}" is not a directory`);
232
- }
233
-
234
- // Find matching files
235
- const foundFiles = await findFiles(resolvedPath, patterns || ["*"], {
236
- exclude,
237
- recursive,
238
- maxResults,
239
- includeHidden,
240
- });
241
-
242
- logger.timingEnd(timingId, "TOOL", "file_find completed");
243
- logger.debug("TOOL", "File search successful", {
244
- searchPath,
245
- foundCount: foundFiles.length,
246
- patterns,
247
- });
248
-
249
- if (foundFiles.length === 0) {
250
- return `No files found matching patterns: ${patterns?.join(", ") || "*"}`;
251
- }
252
-
253
- // Format results
254
- let output = `Found ${foundFiles.length} files matching patterns [${patterns?.join(", ") || "*"}]:\n\n`;
255
-
256
- for (const file of foundFiles) {
257
- const relativePath = path.relative(resolvedPath, file);
258
- output += `${relativePath}\n`;
259
- }
260
-
261
- return output;
262
- } catch (error) {
263
- logger.error(
264
- "TOOL",
265
- "file_find failed",
266
- error instanceof Error ? error : new Error(String(error)),
267
- { searchPath, patterns },
268
- );
269
- return `Error finding files: ${(error as Error).message}`;
270
- }
271
- },
272
- {
273
- name: "find_files",
274
- description: `Discovers files using glob patterns. Respects .gitignore.
11
+ export const findTool = tool(
12
+ async (
13
+ {
14
+ searchPath = ".",
15
+ patterns,
16
+ exclude = [],
17
+ recursive = true,
18
+ maxResults = 100,
19
+ includeHidden = false,
20
+ },
21
+ config
22
+ ) => {
23
+ const timingId = logger.timingStart("find");
24
+
25
+ logger.info("TOOL", "find called", {
26
+ searchPath,
27
+ patterns,
28
+ exclude,
29
+ recursive,
30
+ maxResults,
31
+ includeHidden,
32
+ });
33
+
34
+ try {
35
+ // Get working directory from config context - required for security
36
+ const workingDir = config?.context?.workingDir;
37
+ if (!workingDir) {
38
+ throw new Error(
39
+ "Context with workingDir is required for file operations"
40
+ );
41
+ }
42
+
43
+ // Validate the path to prevent directory traversal
44
+ const resolvedPath = path.resolve(workingDir, searchPath);
45
+ const resolvedWorkingDir = path.resolve(workingDir);
46
+ const relativePath = path.relative(resolvedWorkingDir, resolvedPath);
47
+
48
+ if (relativePath.startsWith("..")) {
49
+ throw new Error(
50
+ `Search path "${searchPath}" attempts to escape the working directory sandbox`
51
+ );
52
+ }
53
+
54
+ // Validate that the search path exists and is a directory
55
+ let stats: import("node:fs").Stats;
56
+ try {
57
+ stats = await fs.stat(resolvedPath);
58
+ } catch (error) {
59
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
60
+ throw new Error(`Search path "${searchPath}" does not exist`);
61
+ }
62
+ throw error;
63
+ }
64
+
65
+ if (!stats.isDirectory()) {
66
+ throw new Error(`Search path "${searchPath}" is not a directory`);
67
+ }
68
+
69
+ // Validate that patterns array is not empty
70
+ if (!patterns || patterns.length === 0) {
71
+ throw new Error(
72
+ 'The "patterns" parameter must contain at least one glob pattern'
73
+ );
74
+ }
75
+
76
+ // Initialize GitIgnoreService
77
+ const gitignore = new GitIgnoreService(workingDir);
78
+ await gitignore.initialize();
79
+
80
+ const foundFiles: string[] = [];
81
+ const excludeGlobs = exclude.map((pattern) => new Glob(pattern));
82
+
83
+ // Process each pattern using Bun's native Glob for accuracy and performance
84
+ for (const pattern of patterns) {
85
+ if (foundFiles.length >= maxResults) {
86
+ break;
87
+ }
88
+
89
+ // If recursive and no directory component in pattern, prepend **/
90
+ const effectivePattern =
91
+ recursive && !pattern.includes("/") && !pattern.includes("**")
92
+ ? `**/${pattern}`
93
+ : pattern;
94
+
95
+ const glob = new Glob(effectivePattern);
96
+
97
+ // Scan using resolvedPath as cwd
98
+ for await (const file of glob.scan({
99
+ cwd: resolvedPath,
100
+ onlyFiles: true,
101
+ dot: includeHidden,
102
+ })) {
103
+ if (foundFiles.length >= maxResults) {
104
+ break;
105
+ }
106
+
107
+ // file is relative to resolvedPath
108
+ const fullPath = path.resolve(resolvedPath, file);
109
+ const relativeToWorkingDir = path.relative(
110
+ resolvedWorkingDir,
111
+ fullPath
112
+ );
113
+
114
+ // 1. Check if file should be ignored by .gitignore
115
+ if (gitignore.shouldIgnore(fullPath, false)) {
116
+ continue;
117
+ }
118
+
119
+ // 2. Check if file should be excluded by manual exclude patterns
120
+ const isExcluded = excludeGlobs.some((eg) =>
121
+ eg.match(relativeToWorkingDir)
122
+ );
123
+ if (isExcluded) {
124
+ continue;
125
+ }
126
+
127
+ if (!foundFiles.includes(relativeToWorkingDir)) {
128
+ foundFiles.push(relativeToWorkingDir);
129
+ }
130
+ }
131
+ }
132
+
133
+ logger.timingEnd(timingId, "TOOL", "find completed");
134
+
135
+ if (foundFiles.length === 0) {
136
+ return `No files found matching patterns: ${patterns.join(", ")}`;
137
+ }
138
+
139
+ foundFiles.sort();
140
+
141
+ // Format results relative to working directory
142
+ let output = `Found ${foundFiles.length} files matching patterns [${patterns.join(", ")}]:\n\n`;
143
+ output += foundFiles.join("\n");
144
+
145
+ return output;
146
+ } catch (error) {
147
+ logger.error(
148
+ "TOOL",
149
+ "find failed",
150
+ error instanceof Error ? error : new Error(String(error)),
151
+ { searchPath, patterns }
152
+ );
153
+
154
+ return formatToolError({
155
+ operation: "find",
156
+ path: searchPath,
157
+ cause: error,
158
+ suggestion: getToolSuggestion("find", searchPath),
159
+ });
160
+ }
161
+ },
162
+ {
163
+ name: "find",
164
+ description: `Discovers files using glob patterns. Respects .gitignore.
275
165
  Usage
276
166
  - Fast file pattern matching command that works with any codebase size
277
167
  - Supports glob patterns like "**/*.js" or "src/**/*.ts"
@@ -280,45 +170,43 @@ Usage
280
170
  - When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Task tool instead
281
171
  - You can call multiple commands in a single response. It is always better to speculatively perform multiple searches in parallel if they are potentially useful.
282
172
  `,
283
- schema: z.object({
284
- searchPath: z
285
- .string()
286
- .describe(
287
- "The directory path to search in, relative to the working directory",
288
- ),
289
- patterns: z
290
- .array(z.string())
291
- .describe(
292
- "Array of glob patterns to match files (e.g., ['*.js', '*.ts'])",
293
- ),
294
- exclude: z
295
- .array(z.string())
296
- .optional()
297
- .default([])
298
- .describe(
299
- "Array of patterns to exclude from results. Defaults to none",
300
- ),
301
- recursive: z
302
- .boolean()
303
- .optional()
304
- .default(true)
305
- .describe(
306
- "Whether to search recursively in subdirectories. Defaults to `true`",
307
- ),
308
- maxResults: z
309
- .number()
310
- .optional()
311
- .default(100)
312
- .describe(
313
- "Maximum number of files to return. Maximum of 100 by default",
314
- ),
315
- includeHidden: z
316
- .boolean()
317
- .optional()
318
- .default(false)
319
- .describe(
320
- "Whether to include hidden files and directories. Defaults to `false`",
321
- ),
322
- }),
323
- },
173
+ schema: z.object({
174
+ searchPath: z
175
+ .string()
176
+ .describe(
177
+ "The directory path to search in, relative to the working directory"
178
+ ),
179
+ patterns: z
180
+ .array(z.string())
181
+ .describe(
182
+ "Array of glob patterns to match files (e.g., ['*.js', '*.ts'])"
183
+ ),
184
+ exclude: z
185
+ .array(z.string())
186
+ .optional()
187
+ .default([])
188
+ .describe(
189
+ "Array of glob patterns to exclude from results. Use full glob patterns for exclusions (e.g., '**/node_modules/**' to exclude a directory and its contents recursively). Defaults to none"
190
+ ),
191
+ recursive: z
192
+ .boolean()
193
+ .optional()
194
+ .default(true)
195
+ .describe(
196
+ "Whether to search recursively in subdirectories. Defaults to `true`"
197
+ ),
198
+ maxResults: z
199
+ .number()
200
+ .optional()
201
+ .default(100)
202
+ .describe("Maximum number of files to return. Defaults to 100"),
203
+ includeHidden: z
204
+ .boolean()
205
+ .optional()
206
+ .default(false)
207
+ .describe(
208
+ "Whether to include hidden files and directories. Defaults to `false`"
209
+ ),
210
+ }),
211
+ }
324
212
  );