@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,154 +1,241 @@
1
+ import path from "node:path";
1
2
  import { tool } from "langchain";
2
3
  import { z } from "zod";
3
- import path from "node:path";
4
- import { logger } from "../utils/logger.js";
4
+ import { formatToolError, getToolSuggestion } from "../utils/error-utils.js";
5
5
  import { isTextFile } from "../utils/file-utils.js";
6
+ import { withLineNumbers } from "../utils/format-utils.js";
7
+ import { logger } from "../utils/logger.js";
6
8
 
7
9
  // Check if file is an image file
8
10
  function isImageFile(filePath: string): boolean {
9
- const imageExtensions = new Set([
10
- ".png",
11
- ".jpg",
12
- ".jpeg",
13
- ".gif",
14
- ".bmp",
15
- ".webp",
16
- ".svg",
17
- ".ico",
18
- ".tiff",
19
- ".tif",
20
- ]);
21
- const ext = path.extname(filePath).toLowerCase();
22
- return imageExtensions.has(ext);
11
+ const imageExtensions = new Set([
12
+ ".png",
13
+ ".jpg",
14
+ ".jpeg",
15
+ ".gif",
16
+ ".bmp",
17
+ ".webp",
18
+ ".svg",
19
+ ".ico",
20
+ ".tiff",
21
+ ".tif",
22
+ ]);
23
+ const ext = path.extname(filePath).toLowerCase();
24
+ return imageExtensions.has(ext);
23
25
  }
24
26
 
25
27
  // Check if file is an audio file
26
28
  function isAudioFile(filePath: string): boolean {
27
- const audioExtensions = new Set([
28
- ".mp3",
29
- ".wav",
30
- ".ogg",
31
- ".flac",
32
- ".m4a",
33
- ".aac",
34
- ".wma",
35
- ".opus",
36
- ".webm",
37
- ]);
38
- const ext = path.extname(filePath).toLowerCase();
39
- return audioExtensions.has(ext);
29
+ const audioExtensions = new Set([
30
+ ".mp3",
31
+ ".wav",
32
+ ".ogg",
33
+ ".flac",
34
+ ".m4a",
35
+ ".aac",
36
+ ".wma",
37
+ ".opus",
38
+ ".webm",
39
+ ]);
40
+ const ext = path.extname(filePath).toLowerCase();
41
+ return audioExtensions.has(ext);
42
+ }
43
+
44
+ // Check if file is a binary file that cannot be read as text
45
+ function isBinaryFile(filePath: string): boolean {
46
+ const binaryExtensions = new Set([
47
+ ".pdf",
48
+ ".zip",
49
+ ".gz",
50
+ ".tar",
51
+ ".rar",
52
+ ".7z",
53
+ ".exe",
54
+ ".dll",
55
+ ".so",
56
+ ".dylib",
57
+ ".class",
58
+ ".jar",
59
+ ".war",
60
+ ".ear",
61
+ ]);
62
+ const ext = path.extname(filePath).toLowerCase();
63
+ return binaryExtensions.has(ext);
40
64
  }
41
65
 
42
- // Safe file read with encoding detection
43
- async function readFileContent(filePath: string): Promise<string> {
44
- try {
45
- return await Bun.file(filePath).text();
46
- } catch (error: unknown) {
47
- const errorMessage =
48
- error instanceof Error ? error.message : "Unknown error";
49
- throw new Error(`Failed to read file: ${errorMessage}`);
50
- }
66
+ /**
67
+ * Reads lines from a file within a specific range using a streaming approach.
68
+ * This is memory-efficient for large files as it avoids loading the entire file.
69
+ */
70
+ async function readLinesInRange(
71
+ filePath: string,
72
+ viewRange?: [number, number]
73
+ ): Promise<{ lines: string[]; totalLines: number }> {
74
+ const file = Bun.file(filePath);
75
+ const stream = file.stream();
76
+ const reader = stream.getReader();
77
+ const decoder = new TextDecoder();
78
+
79
+ const lines: string[] = [];
80
+ let currentLine = 1;
81
+ const startLine = viewRange ? viewRange[0] : 1;
82
+ const endLine = viewRange ? viewRange[1] : -1;
83
+ let totalLines = 0;
84
+
85
+ let partialLine = "";
86
+
87
+ try {
88
+ while (true) {
89
+ let chunk: string;
90
+ try {
91
+ const { done, value } = await reader.read();
92
+ if (done) {
93
+ if (partialLine) {
94
+ totalLines++;
95
+ if (
96
+ currentLine >= startLine &&
97
+ (endLine === -1 || currentLine <= endLine)
98
+ ) {
99
+ lines.push(partialLine);
100
+ }
101
+ }
102
+ break;
103
+ }
104
+ chunk = decoder.decode(value, { stream: true });
105
+ } catch (readError) {
106
+ reader.releaseLock();
107
+ throw new Error(
108
+ `Failed to read file ${filePath}: ${(readError as Error).message}`
109
+ );
110
+ }
111
+
112
+ const chunkLines = (partialLine + chunk).split("\n");
113
+ partialLine = chunkLines.pop() || "";
114
+
115
+ for (const line of chunkLines) {
116
+ if (
117
+ currentLine >= startLine &&
118
+ (endLine === -1 || currentLine <= endLine)
119
+ ) {
120
+ lines.push(line);
121
+ }
122
+ currentLine++;
123
+ totalLines++;
124
+
125
+ // Optimization: Stop reading if we've passed the requested range
126
+ if (endLine !== -1 && currentLine > endLine) {
127
+ await reader.cancel();
128
+ return { lines, totalLines: -1 };
129
+ }
130
+ }
131
+ }
132
+ } finally {
133
+ reader.releaseLock();
134
+ }
135
+
136
+ return { lines, totalLines };
51
137
  }
52
138
 
53
139
  // Create the modernized tool using the tool() function
54
- export const fileReadTool = tool(
55
- async ({ filePath }, config) => {
56
- const timingId = logger.timingStart("fileRead");
57
-
58
- logger.info("TOOL", "file_read called", { filePath });
59
-
60
- try {
61
- // Get working directory from config context - required for security
62
- const workingDir = config?.context?.workingDir;
63
- if (!workingDir) {
64
- throw new Error(
65
- "Context with workingDir is required for file operations",
66
- );
67
- }
68
- logger.debug("TOOL", "Working directory", {
69
- workingDir: workingDir.replace(Bun.env.HOME || "", "~"),
70
- });
71
-
72
- // Validate the path to prevent directory traversal
73
- const resolvedPath = path.resolve(workingDir, filePath);
74
- const resolvedWorkingDir = path.resolve(workingDir);
75
- const relativePath = path.relative(resolvedWorkingDir, resolvedPath);
76
-
77
- logger.debug("TOOL", "Path validation", {
78
- resolvedPath: resolvedPath.replace(Bun.env.HOME || "", "~"),
79
- resolvedWorkingDir: resolvedWorkingDir.replace(Bun.env.HOME || "", "~"),
80
- relativePath,
81
- validated: !relativePath.startsWith(".."),
82
- });
83
-
84
- if (relativePath.startsWith("..")) {
85
- logger.error(
86
- "PATH",
87
- "File path escapes working directory sandbox",
88
- undefined,
89
- { filePath, relativePath },
90
- );
91
- throw new Error(
92
- `File path "${filePath}" attempts to escape the working directory sandbox`,
93
- );
94
- }
95
-
96
- // Check if it's a media file
97
- if (isImageFile(resolvedPath) || isAudioFile(resolvedPath)) {
98
- logger.debug("TOOL", "File is media file", {
99
- filePath,
100
- fileType: isImageFile(resolvedPath) ? "image" : "audio",
101
- });
102
- return `This is a media file (${isImageFile(resolvedPath) ? "image" : "audio"}). Media files cannot be read as text. Path: ${filePath}`;
103
- }
104
-
105
- // Check if it's a text file
106
- const isText = await isTextFile(resolvedPath);
107
- if (!isText) {
108
- logger.debug("TOOL", "File is not a text file", { filePath });
109
- return `This file is not a text file and cannot be read as text. Path: ${filePath}`;
110
- }
111
-
112
- logger.debug("TOOL", "File type validated as text", { filePath });
113
-
114
- // Read file content
115
- const content = await readFileContent(resolvedPath);
116
-
117
- // Limit content size to avoid overwhelming the AI
118
- const maxContentLength = 50_000; // 50KB limit
119
- if (content.length > maxContentLength) {
120
- logger.debug("TOOL", "File content truncated", {
121
- filePath,
122
- originalSize: content.length,
123
- maxSize: maxContentLength,
124
- });
125
- logger.timingEnd(timingId, "TOOL", "file_read completed (truncated)");
126
- return `File content is too large (${content.length} characters). First ${maxContentLength} characters:\n\n${content.substring(0, maxContentLength)}\n\n[Content truncated due to length]`;
127
- }
128
-
129
- logger.timingEnd(timingId, "TOOL", "file_read completed");
130
- logger.debug("TOOL", "File read successful", {
131
- filePath,
132
- contentLength: content.length,
133
- });
134
-
135
- return `Content of file: ${filePath}\n\n${content}`;
136
- } catch (error) {
137
- logger.error(
138
- "TOOL",
139
- "file_read failed",
140
- error instanceof Error ? error : new Error(String(error)),
141
- { filePath },
142
- );
143
- return `Error reading file: ${(error as Error).message}`;
144
- }
145
- },
146
- {
147
- name: "file_read",
148
- description:
149
- "Read the contents of a file. Use this to examine the content of a specific file.",
150
- schema: z.object({
151
- filePath: z.string().describe("The path to the file to read"),
152
- }),
153
- },
140
+ export const viewTool = tool(
141
+ async ({ filePath, viewRange }, config) => {
142
+ const timingId = logger.timingStart("view");
143
+
144
+ logger.info("TOOL", "view called", { filePath, viewRange });
145
+
146
+ try {
147
+ // Validate viewRange if provided
148
+ if (viewRange) {
149
+ const [start, end] = viewRange;
150
+ if (start < 1) {
151
+ throw new Error(
152
+ `Invalid viewRange: start must be >= 1, got ${start}`
153
+ );
154
+ }
155
+ if (end !== -1 && end < start) {
156
+ throw new Error(
157
+ `Invalid viewRange: end (${end}) must be >= start (${start}) or -1 for end of file`
158
+ );
159
+ }
160
+ }
161
+
162
+ // Get working directory from config context - required for security
163
+ const workingDir = config?.context?.workingDir;
164
+ if (!workingDir) {
165
+ throw new Error(
166
+ "Context with workingDir is required for file operations"
167
+ );
168
+ }
169
+
170
+ // Validate the path to prevent directory traversal
171
+ const resolvedPath = path.resolve(workingDir, filePath);
172
+ const resolvedWorkingDir = path.resolve(workingDir);
173
+ const relativePath = path.relative(resolvedWorkingDir, resolvedPath);
174
+
175
+ if (relativePath.startsWith("..")) {
176
+ throw new Error(
177
+ `File path "${filePath}" attempts to escape the working directory sandbox`
178
+ );
179
+ }
180
+
181
+ // Check if it's a media file
182
+ if (isImageFile(resolvedPath) || isAudioFile(resolvedPath)) {
183
+ return `This is a media file (${isImageFile(resolvedPath) ? "image" : "audio"}). Media files cannot be read as text. Path: ${filePath}`;
184
+ }
185
+
186
+ // Check if it's a binary file
187
+ if (isBinaryFile(resolvedPath)) {
188
+ return `This is a binary file (${path.extname(filePath)}). Binary files cannot be read as text. Path: ${filePath}`;
189
+ }
190
+
191
+ // Check if it's a text file
192
+ const isText = await isTextFile(resolvedPath);
193
+ if (!isText) {
194
+ return `This file is not a text file and cannot be read as text. Path: ${filePath}`;
195
+ }
196
+
197
+ // Read file content within range using streaming
198
+ const { lines } = await readLinesInRange(resolvedPath, viewRange);
199
+
200
+ if (lines.length === 0) {
201
+ return "[File is empty]";
202
+ }
203
+
204
+ // Format content with correct line numbers
205
+ const startLine = viewRange ? Math.max(1, viewRange[0]) : 1;
206
+ const formattedContent = withLineNumbers(lines, startLine);
207
+
208
+ logger.timingEnd(timingId, "TOOL", "view completed");
209
+
210
+ return formattedContent;
211
+ } catch (error) {
212
+ logger.error(
213
+ "TOOL",
214
+ "view failed",
215
+ error instanceof Error ? error : new Error(String(error)),
216
+ { filePath }
217
+ );
218
+
219
+ return formatToolError({
220
+ operation: "view",
221
+ path: filePath,
222
+ cause: error,
223
+ suggestion: getToolSuggestion("view", filePath),
224
+ });
225
+ }
226
+ },
227
+ {
228
+ name: "view",
229
+ description:
230
+ "Read the contents of a file. Use this to examine the content of a specific file.",
231
+ schema: z.object({
232
+ filePath: z.string().describe("The path to the file to read"),
233
+ viewRange: z
234
+ .tuple([z.number(), z.number()])
235
+ .optional()
236
+ .describe(
237
+ "Optional tuple of [start, end] line numbers to display. Use -1 for end of file."
238
+ ),
239
+ }),
240
+ }
154
241
  );