@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,212 +1,255 @@
|
|
|
1
|
-
import { tool } from "langchain";
|
|
2
|
-
import { z } from "zod";
|
|
3
1
|
import fs from "node:fs/promises";
|
|
4
2
|
import path from "node:path";
|
|
3
|
+
import { tool } from "langchain";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { formatToolError, getToolSuggestion } from "../utils/error-utils.js";
|
|
6
|
+
import { getFileLineCount } from "../utils/file-utils.js";
|
|
7
|
+
import {
|
|
8
|
+
type FileSystemEntry,
|
|
9
|
+
formatDirectoryTree,
|
|
10
|
+
} from "../utils/format-utils.js";
|
|
11
|
+
import { GitIgnoreService } from "../utils/gitignore-service.js";
|
|
5
12
|
import { logger } from "../utils/logger.js";
|
|
6
13
|
|
|
7
|
-
// Define types for file system operations
|
|
8
|
-
interface FileSystemEntry {
|
|
9
|
-
name: string;
|
|
10
|
-
path: string;
|
|
11
|
-
isDirectory: boolean;
|
|
12
|
-
size?: number | undefined;
|
|
13
|
-
modified?: Date | undefined;
|
|
14
|
-
permissions?: string | undefined;
|
|
15
|
-
lineCount?: number | undefined;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface DirectoryListing {
|
|
19
|
-
entries: FileSystemEntry[];
|
|
20
|
-
totalCount: number;
|
|
21
|
-
filteredCount?: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Simplified function to check if a file should be ignored (basic .gitignore handling)
|
|
25
|
-
async function shouldIgnoreFile(filePath: string): Promise<boolean> {
|
|
26
|
-
const basename = path.basename(filePath);
|
|
27
|
-
|
|
28
|
-
// Basic checks for common files/directories to ignore
|
|
29
|
-
if (basename.startsWith(".") && basename !== ".env") {
|
|
30
|
-
// Check for common gitignored items
|
|
31
|
-
const ignoredItems = [
|
|
32
|
-
"node_modules",
|
|
33
|
-
".git",
|
|
34
|
-
".DS_Store",
|
|
35
|
-
".idea",
|
|
36
|
-
".vscode",
|
|
37
|
-
".pytest_cache",
|
|
38
|
-
];
|
|
39
|
-
if (ignoredItems.includes(basename)) {
|
|
40
|
-
return true;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Check for common build/output directories
|
|
45
|
-
const commonBuildDirs = ["dist", "build", "out", "target", "coverage"];
|
|
46
|
-
if (
|
|
47
|
-
commonBuildDirs.includes(basename) &&
|
|
48
|
-
(await fs
|
|
49
|
-
.stat(filePath)
|
|
50
|
-
.then((s) => s.isDirectory())
|
|
51
|
-
.catch(() => false))
|
|
52
|
-
) {
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
14
|
/**
|
|
60
|
-
*
|
|
15
|
+
* Improved listDirectory that sorts siblings before recursing to maintain DFS tree order.
|
|
16
|
+
* Processes line counts in parallel for performance.
|
|
61
17
|
*/
|
|
62
|
-
async function
|
|
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
|
-
|
|
18
|
+
async function listDirectoryDFS(
|
|
19
|
+
dirPath: string,
|
|
20
|
+
includeHidden = false,
|
|
21
|
+
recursive = false,
|
|
22
|
+
maxDepth = 1,
|
|
23
|
+
currentDepth = 0,
|
|
24
|
+
gitignore?: GitIgnoreService
|
|
25
|
+
): Promise<FileSystemEntry[]> {
|
|
26
|
+
// At maxDepth=1:
|
|
27
|
+
// - currentDepth=0: process entries, DON'T recurse (0 + 1 < 1 is FALSE)
|
|
28
|
+
// - currentDepth=1: return early (stop recursion)
|
|
29
|
+
if (currentDepth >= maxDepth) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
34
|
+
|
|
35
|
+
// Filter entries
|
|
36
|
+
const filteredEntries: import("node:fs").Dirent[] = [];
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
39
|
+
|
|
40
|
+
// 1. Check hidden
|
|
41
|
+
if (!includeHidden && entry.name.startsWith(".")) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 2. Check gitignore
|
|
46
|
+
if (gitignore?.shouldIgnore(fullPath, entry.isDirectory())) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
filteredEntries.push(entry);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Sort: directories first, then by name
|
|
54
|
+
filteredEntries.sort((a, b) => {
|
|
55
|
+
if (a.isDirectory() !== b.isDirectory()) {
|
|
56
|
+
return a.isDirectory() ? -1 : 1;
|
|
57
|
+
}
|
|
58
|
+
return a.name.localeCompare(b.name);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const fileEntries: FileSystemEntry[] = [];
|
|
62
|
+
|
|
63
|
+
for (const entry of filteredEntries) {
|
|
64
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
65
|
+
try {
|
|
66
|
+
const stats = await fs.stat(fullPath);
|
|
67
|
+
const metadata: FileSystemEntry = {
|
|
68
|
+
name: entry.name,
|
|
69
|
+
path: fullPath,
|
|
70
|
+
isDirectory: stats.isDirectory(),
|
|
71
|
+
...(stats.isFile() && { size: stats.size }),
|
|
72
|
+
depth: currentDepth,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
fileEntries.push(metadata);
|
|
76
|
+
} catch {
|
|
77
|
+
// Skip files that can't be accessed
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Calculate line counts in parallel for all files at this level, with throttling to avoid EMFILE
|
|
82
|
+
const CONCURRENCY_LIMIT = 50;
|
|
83
|
+
const files = fileEntries.filter((e) => !e.isDirectory);
|
|
84
|
+
|
|
85
|
+
for (let i = 0; i < files.length; i += CONCURRENCY_LIMIT) {
|
|
86
|
+
const batch = files.slice(i, i + CONCURRENCY_LIMIT);
|
|
87
|
+
|
|
88
|
+
// Process batch with EMFILE retry logic
|
|
89
|
+
let retries = 3;
|
|
90
|
+
while (retries > 0) {
|
|
91
|
+
try {
|
|
92
|
+
await Promise.all(
|
|
93
|
+
batch.map(async (e) => {
|
|
94
|
+
e.lineCount = await getFileLineCount(e.path);
|
|
95
|
+
})
|
|
96
|
+
);
|
|
97
|
+
break; // Success, exit retry loop
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if ((error as NodeJS.ErrnoException).code === "EMFILE" && retries > 0) {
|
|
100
|
+
retries--;
|
|
101
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
102
|
+
} else {
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Recursively process subdirectories
|
|
110
|
+
const resultEntries: FileSystemEntry[] = [];
|
|
111
|
+
for (const entry of fileEntries) {
|
|
112
|
+
// Determine if we should recurse into this entry
|
|
113
|
+
// Only recurse if: it's a directory, recursive is true, and next depth is within maxDepth
|
|
114
|
+
const willRecurse =
|
|
115
|
+
entry.isDirectory && recursive && currentDepth + 1 < maxDepth;
|
|
116
|
+
|
|
117
|
+
// Add the entry if:
|
|
118
|
+
// 1. It's a file (files are always included)
|
|
119
|
+
// 2. It's a directory AND (recursive is false OR we will recurse into it)
|
|
120
|
+
// 3. It's a directory AND we're at currentDepth=0 (show top-level directories but not their contents)
|
|
121
|
+
if (
|
|
122
|
+
!(entry.isDirectory && recursive) ||
|
|
123
|
+
willRecurse ||
|
|
124
|
+
(entry.isDirectory && currentDepth === 0)
|
|
125
|
+
) {
|
|
126
|
+
resultEntries.push(entry);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Recurse if allowed
|
|
130
|
+
if (willRecurse) {
|
|
131
|
+
const subEntries = await listDirectoryDFS(
|
|
132
|
+
entry.path,
|
|
133
|
+
includeHidden,
|
|
134
|
+
recursive,
|
|
135
|
+
maxDepth,
|
|
136
|
+
currentDepth + 1,
|
|
137
|
+
gitignore
|
|
138
|
+
);
|
|
139
|
+
resultEntries.push(...subEntries);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return resultEntries;
|
|
118
144
|
}
|
|
119
145
|
|
|
120
146
|
// Create the modernized tool using the tool() function
|
|
121
|
-
export const
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
147
|
+
export const listTool = tool(
|
|
148
|
+
async (
|
|
149
|
+
{
|
|
150
|
+
directoryPath = ".",
|
|
151
|
+
includeHidden = false,
|
|
152
|
+
recursive = false,
|
|
153
|
+
maxDepth = 1,
|
|
154
|
+
},
|
|
155
|
+
config
|
|
156
|
+
) => {
|
|
157
|
+
const timingId = logger.timingStart("list");
|
|
158
|
+
|
|
159
|
+
logger.info("TOOL", "list called", {
|
|
160
|
+
directoryPath,
|
|
161
|
+
includeHidden,
|
|
162
|
+
recursive,
|
|
163
|
+
maxDepth,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
// Get working directory from config context - required for security
|
|
168
|
+
const workingDir = config?.context?.workingDir;
|
|
169
|
+
if (!workingDir) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
"Context with workingDir is required for file operations"
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Validate path to prevent directory traversal
|
|
176
|
+
const resolvedPath = path.resolve(workingDir, directoryPath);
|
|
177
|
+
const resolvedWorkingDir = path.resolve(workingDir);
|
|
178
|
+
const relativePath = path.relative(resolvedWorkingDir, resolvedPath);
|
|
179
|
+
|
|
180
|
+
// Check if resolved path escapes working directory
|
|
181
|
+
if (relativePath.startsWith("..")) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`Directory path "${directoryPath}" attempts to escape working directory sandbox`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Initialize GitIgnoreService
|
|
188
|
+
const gitignore = new GitIgnoreService(workingDir);
|
|
189
|
+
await gitignore.initialize();
|
|
190
|
+
|
|
191
|
+
// List the directory using DFS to maintain tree order
|
|
192
|
+
const entries = await listDirectoryDFS(
|
|
193
|
+
resolvedPath,
|
|
194
|
+
includeHidden,
|
|
195
|
+
recursive,
|
|
196
|
+
maxDepth,
|
|
197
|
+
0,
|
|
198
|
+
gitignore
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// Format the result for the AI
|
|
202
|
+
const treeOutput = formatDirectoryTree(entries);
|
|
203
|
+
|
|
204
|
+
// Use relative path for display
|
|
205
|
+
const displayPath = relativePath === "" ? "." : relativePath;
|
|
206
|
+
let result = `Contents of directory: ${displayPath}\n\n`;
|
|
207
|
+
result += `Total entries: ${entries.length}\n\n`;
|
|
208
|
+
result += treeOutput;
|
|
209
|
+
|
|
210
|
+
logger.timingEnd(timingId, "TOOL", "list completed");
|
|
211
|
+
return result;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
logger.error(
|
|
214
|
+
"TOOL",
|
|
215
|
+
"list failed",
|
|
216
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
217
|
+
{ directoryPath }
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
return formatToolError({
|
|
221
|
+
operation: "list",
|
|
222
|
+
path: directoryPath,
|
|
223
|
+
cause: error,
|
|
224
|
+
suggestion: getToolSuggestion("list", directoryPath),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: "list",
|
|
230
|
+
description:
|
|
231
|
+
"List the contents of a directory with metadata. Use this to understand the structure of a repository or directory.",
|
|
232
|
+
schema: z.object({
|
|
233
|
+
directoryPath: z.string().describe("The directory path to list"),
|
|
234
|
+
includeHidden: z
|
|
235
|
+
.boolean()
|
|
236
|
+
.optional()
|
|
237
|
+
.default(false)
|
|
238
|
+
.describe(
|
|
239
|
+
"Whether to include hidden files and directories. Defaults to `false`"
|
|
240
|
+
),
|
|
241
|
+
recursive: z
|
|
242
|
+
.boolean()
|
|
243
|
+
.optional()
|
|
244
|
+
.default(false)
|
|
245
|
+
.describe(
|
|
246
|
+
"Whether to list subdirectories recursively. Defaults to `false`"
|
|
247
|
+
),
|
|
248
|
+
maxDepth: z
|
|
249
|
+
.number()
|
|
250
|
+
.optional()
|
|
251
|
+
.default(1)
|
|
252
|
+
.describe("Maximum depth for recursive listing. Defaults to 1"),
|
|
253
|
+
}),
|
|
254
|
+
}
|
|
212
255
|
);
|