@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.
- package/README.md +4 -16
- package/dist/agents/context-schema.d.ts +1 -1
- package/dist/agents/context-schema.d.ts.map +1 -1
- package/dist/agents/context-schema.js +5 -2
- package/dist/agents/context-schema.js.map +1 -1
- package/dist/agents/react-agent.d.ts.map +1 -1
- package/dist/agents/react-agent.js +63 -170
- package/dist/agents/react-agent.js.map +1 -1
- package/dist/agents/tool-runtime.d.ts.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +53 -49
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +115 -69
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +246 -150
- package/dist/index.js.map +1 -1
- package/dist/tools/file-finding.tool.d.ts +1 -1
- package/dist/tools/file-finding.tool.d.ts.map +1 -1
- package/dist/tools/file-finding.tool.js +70 -130
- package/dist/tools/file-finding.tool.js.map +1 -1
- package/dist/tools/file-listing.tool.d.ts +7 -1
- package/dist/tools/file-listing.tool.d.ts.map +1 -1
- package/dist/tools/file-listing.tool.js +96 -80
- package/dist/tools/file-listing.tool.js.map +1 -1
- package/dist/tools/file-reading.tool.d.ts +4 -1
- package/dist/tools/file-reading.tool.d.ts.map +1 -1
- package/dist/tools/file-reading.tool.js +107 -45
- package/dist/tools/file-reading.tool.js.map +1 -1
- package/dist/tools/grep-content.tool.d.ts +13 -1
- package/dist/tools/grep-content.tool.d.ts.map +1 -1
- package/dist/tools/grep-content.tool.js +186 -144
- package/dist/tools/grep-content.tool.js.map +1 -1
- package/dist/utils/error-utils.d.ts +9 -0
- package/dist/utils/error-utils.d.ts.map +1 -0
- package/dist/utils/error-utils.js +61 -0
- package/dist/utils/error-utils.js.map +1 -0
- package/dist/utils/file-utils.d.ts +1 -0
- package/dist/utils/file-utils.d.ts.map +1 -1
- package/dist/utils/file-utils.js +81 -9
- package/dist/utils/file-utils.js.map +1 -1
- package/dist/utils/format-utils.d.ts +25 -0
- package/dist/utils/format-utils.d.ts.map +1 -0
- package/dist/utils/format-utils.js +111 -0
- package/dist/utils/format-utils.js.map +1 -0
- package/dist/utils/gitignore-service.d.ts +10 -0
- package/dist/utils/gitignore-service.d.ts.map +1 -0
- package/dist/utils/gitignore-service.js +91 -0
- package/dist/utils/gitignore-service.js.map +1 -0
- package/dist/utils/logger.d.ts +2 -2
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +35 -34
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/path-utils.js +3 -3
- package/dist/utils/path-utils.js.map +1 -1
- package/package.json +1 -1
- package/src/agents/context-schema.ts +5 -2
- package/src/agents/react-agent.ts +694 -784
- package/src/agents/tool-runtime.ts +4 -4
- package/src/cli.ts +95 -57
- package/src/config.ts +192 -90
- package/src/index.ts +402 -180
- package/src/tools/file-finding.tool.ts +198 -310
- package/src/tools/file-listing.tool.ts +245 -202
- package/src/tools/file-reading.tool.ts +225 -138
- package/src/tools/grep-content.tool.ts +387 -307
- package/src/utils/error-utils.ts +95 -0
- package/src/utils/file-utils.ts +104 -19
- package/src/utils/format-utils.ts +190 -0
- package/src/utils/gitignore-service.ts +123 -0
- package/src/utils/logger.ts +112 -77
- 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
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
);
|