cowork-cli 0.0.1 → 1.0.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 +78 -0
- package/bin/cli.js +43 -0
- package/package.json +33 -5
- package/src/configs/config.json +11 -0
- package/src/configs/sys.txt +33 -0
- package/src/engine/client.js +26 -0
- package/src/engine/models/BaseModel.js +228 -0
- package/src/engine/models/default.js +8 -0
- package/src/engine/models/gemini.js +20 -0
- package/src/engine/run.js +50 -0
- package/src/engine/tools/askConfirm.js +34 -0
- package/src/engine/tools/askUser.js +32 -0
- package/src/engine/tools/findDir.js +73 -0
- package/src/engine/tools/findFile.js +74 -0
- package/src/engine/tools/index.js +204 -0
- package/src/engine/tools/listTools.js +79 -0
- package/src/engine/tools/projectTree.js +89 -0
- package/src/engine/tools/readDir.js +32 -0
- package/src/engine/tools/readFile.js +41 -0
- package/src/engine/tools/readFileChunk.js +48 -0
- package/src/engine/tools/searchText.js +133 -0
- package/src/engine/tools/webFetch.js +154 -0
- package/src/main.js +50 -0
- package/src/utils/configManager.js +71 -0
- package/src/utils/fsUtils.js +37 -0
- package/src/utils/helpMsg.js +23 -0
- package/src/utils/logger.js +56 -0
- package/src/utils/outputFormatter.js +72 -0
- package/src/utils/ui.js +582 -0
- package/index.js +0 -2
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getIgnorePatterns, shouldIgnore } from '../../utils/fsUtils.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* findFile tool: Finds files by name using regex.
|
|
7
|
+
* @param {Object} args
|
|
8
|
+
* @param {string} args.pattern Regex pattern to match filenames.
|
|
9
|
+
* @param {string} args.dirPath Root directory to search (default: current directory).
|
|
10
|
+
* @param {boolean} args.recursive Whether to search subdirectories (default: true).
|
|
11
|
+
* @param {number} args.limit Maximum number of results (default: 15, max: 15).
|
|
12
|
+
*/
|
|
13
|
+
export default async function findFile({ pattern, dirPath = '.', recursive = true, limit = 15 }) {
|
|
14
|
+
try {
|
|
15
|
+
if (!pattern) return "Error: Search pattern cannot be empty.";
|
|
16
|
+
|
|
17
|
+
// Enforce max limit of 15
|
|
18
|
+
const finalLimit = Math.min(limit, 15);
|
|
19
|
+
|
|
20
|
+
let regex;
|
|
21
|
+
try {
|
|
22
|
+
regex = new RegExp(pattern, 'i');
|
|
23
|
+
} catch (e) {
|
|
24
|
+
return `Error: Invalid regex pattern '${pattern}': ${e.message}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const ignoreList = await getIgnorePatterns();
|
|
28
|
+
const results = [];
|
|
29
|
+
let totalFound = 0;
|
|
30
|
+
|
|
31
|
+
async function walk(currentPath) {
|
|
32
|
+
if (results.length >= finalLimit) return;
|
|
33
|
+
|
|
34
|
+
let items;
|
|
35
|
+
try {
|
|
36
|
+
items = await fs.readdir(currentPath, { withFileTypes: true });
|
|
37
|
+
} catch (err) {
|
|
38
|
+
return; // Skip unreadable directories
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const item of items) {
|
|
42
|
+
if (results.length >= finalLimit) break;
|
|
43
|
+
if (shouldIgnore(item.name, ignoreList)) continue;
|
|
44
|
+
|
|
45
|
+
const fullPath = path.join(currentPath, item.name);
|
|
46
|
+
|
|
47
|
+
if (item.isDirectory()) {
|
|
48
|
+
if (recursive) {
|
|
49
|
+
await walk(fullPath);
|
|
50
|
+
}
|
|
51
|
+
} else if (item.isFile()) {
|
|
52
|
+
if (regex.test(item.name)) {
|
|
53
|
+
results.push(path.relative(process.cwd(), fullPath));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await walk(dirPath);
|
|
60
|
+
|
|
61
|
+
if (results.length === 0) {
|
|
62
|
+
return `No files found matching "${pattern}" in "${dirPath}".`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let output = results.join('\n');
|
|
66
|
+
if (results.length >= finalLimit) {
|
|
67
|
+
output += `\n[Warning: Truncated at ${finalLimit} matches]`;
|
|
68
|
+
}
|
|
69
|
+
return output;
|
|
70
|
+
|
|
71
|
+
} catch (err) {
|
|
72
|
+
return `Error searching for files: ${err.message}`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import readFile from './readFile.js';
|
|
2
|
+
import readDir from './readDir.js';
|
|
3
|
+
import projectTree from './projectTree.js';
|
|
4
|
+
import readFileChunk from './readFileChunk.js';
|
|
5
|
+
import searchText from './searchText.js';
|
|
6
|
+
import webFetch from './webFetch.js';
|
|
7
|
+
import listTools from './listTools.js';
|
|
8
|
+
import findFile from './findFile.js';
|
|
9
|
+
import findDir from './findDir.js';
|
|
10
|
+
import askUser from './askUser.js';
|
|
11
|
+
import askConfirm from './askConfirm.js';
|
|
12
|
+
|
|
13
|
+
export const toolDefinitions = [
|
|
14
|
+
{
|
|
15
|
+
type: "function",
|
|
16
|
+
function: {
|
|
17
|
+
name: "readFile",
|
|
18
|
+
description: "Read a file's full content. Best for files <1MB.",
|
|
19
|
+
parameters: {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
filePath: { type: "string", description: "Path to the file." }
|
|
23
|
+
},
|
|
24
|
+
required: ["filePath"]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
type: "function",
|
|
30
|
+
function: {
|
|
31
|
+
name: "readDir",
|
|
32
|
+
description: "List directory contents, including types (DIR/FILE).",
|
|
33
|
+
parameters: {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: {
|
|
36
|
+
dirPath: { type: "string", description: "Path to the directory." }
|
|
37
|
+
},
|
|
38
|
+
required: ["dirPath"]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
type: "function",
|
|
44
|
+
function: {
|
|
45
|
+
name: "projectTree",
|
|
46
|
+
description: "Generate a visual directory tree. Respects .gitignore.",
|
|
47
|
+
parameters: {
|
|
48
|
+
type: "object",
|
|
49
|
+
properties: {
|
|
50
|
+
dirPath: { type: "string", description: "The folder to act as root for the tree." }
|
|
51
|
+
},
|
|
52
|
+
required: ["dirPath"]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
type: "function",
|
|
58
|
+
function: {
|
|
59
|
+
name: "readFileChunk",
|
|
60
|
+
description: "Read specific line ranges. Ideal for large files.",
|
|
61
|
+
parameters: {
|
|
62
|
+
type: "object",
|
|
63
|
+
properties: {
|
|
64
|
+
filePath: { type: "string", description: "Path to the file." },
|
|
65
|
+
startLine: { type: "number", description: "1-based start line." },
|
|
66
|
+
endLine: { type: "number", description: "1-based end line (inclusive)." }
|
|
67
|
+
},
|
|
68
|
+
required: ["filePath", "startLine", "endLine"]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
type: "function",
|
|
74
|
+
function: {
|
|
75
|
+
name: "searchText",
|
|
76
|
+
description: "Regex search in files/folders. Supports recursion and .gitignore.",
|
|
77
|
+
parameters: {
|
|
78
|
+
type: "object",
|
|
79
|
+
properties: {
|
|
80
|
+
pattern: { type: "string", description: "Regex or text pattern." },
|
|
81
|
+
path: { type: "string", description: "File or directory to search." },
|
|
82
|
+
recursive: { type: "boolean", description: "Search subdirectories? (default: false)" }
|
|
83
|
+
},
|
|
84
|
+
required: ["pattern", "path"]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
type: "function",
|
|
90
|
+
function: {
|
|
91
|
+
name: "webFetch",
|
|
92
|
+
description: "Fetch and clean text from a URL. Ideal for docs/APIs.",
|
|
93
|
+
parameters: {
|
|
94
|
+
type: "object",
|
|
95
|
+
properties: {
|
|
96
|
+
url: { type: "string", description: "Full HTTP/HTTPS URL." }
|
|
97
|
+
},
|
|
98
|
+
required: ["url"]
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
type: "function",
|
|
104
|
+
function: {
|
|
105
|
+
name: "listTools",
|
|
106
|
+
description: "List all available tools and usage guidelines.",
|
|
107
|
+
parameters: {
|
|
108
|
+
type: "object",
|
|
109
|
+
properties: {},
|
|
110
|
+
required: []
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
type: "function",
|
|
116
|
+
function: {
|
|
117
|
+
name: "findFile",
|
|
118
|
+
description: "Find files by name using regex. Supports recursion and .gitignore.",
|
|
119
|
+
parameters: {
|
|
120
|
+
type: "object",
|
|
121
|
+
properties: {
|
|
122
|
+
pattern: { type: "string", description: "Regex pattern to match filenames." },
|
|
123
|
+
dirPath: { type: "string", description: "Root directory to search (default: '.')." },
|
|
124
|
+
recursive: { type: "boolean", description: "Search subdirectories? (default: true)" },
|
|
125
|
+
limit: { type: "number", description: "Maximum number of results (default: 15, max: 15)." }
|
|
126
|
+
},
|
|
127
|
+
required: ["pattern"]
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
type: "function",
|
|
133
|
+
function: {
|
|
134
|
+
name: "findDir",
|
|
135
|
+
description: "Find directories by name using regex. Supports recursion and .gitignore.",
|
|
136
|
+
parameters: {
|
|
137
|
+
type: "object",
|
|
138
|
+
properties: {
|
|
139
|
+
pattern: { type: "string", description: "Regex pattern to match directory names." },
|
|
140
|
+
dirPath: { type: "string", description: "Root directory to search (default: '.')." },
|
|
141
|
+
recursive: { type: "boolean", description: "Search subdirectories? (default: true)" },
|
|
142
|
+
limit: { type: "number", description: "Maximum number of results (default: 15, max: 15)." }
|
|
143
|
+
},
|
|
144
|
+
required: ["pattern"]
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
type: "function",
|
|
150
|
+
function: {
|
|
151
|
+
name: "askUser",
|
|
152
|
+
description: "Ask the user an open-ended question via the terminal and get a free-text response.",
|
|
153
|
+
parameters: {
|
|
154
|
+
type: "object",
|
|
155
|
+
properties: {
|
|
156
|
+
question: { type: "string", description: "The question to ask the user." }
|
|
157
|
+
},
|
|
158
|
+
required: ["question"]
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
type: "function",
|
|
164
|
+
function: {
|
|
165
|
+
name: "askConfirm",
|
|
166
|
+
description: "Ask the user a yes/no confirmation question. Use this instead of askUser when only a boolean decision is needed. Returns { confirmed: true } for yes, { confirmed: false } for no, and { confirmed: false, dismissed: true } if the user cancels with Ctrl+C.",
|
|
167
|
+
parameters: {
|
|
168
|
+
type: "object",
|
|
169
|
+
properties: {
|
|
170
|
+
question: { type: "string", description: "The yes/no question to ask the user." }
|
|
171
|
+
},
|
|
172
|
+
required: ["question"]
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
const toolImplementations = {
|
|
179
|
+
readFile,
|
|
180
|
+
readDir,
|
|
181
|
+
projectTree,
|
|
182
|
+
readFileChunk,
|
|
183
|
+
searchText,
|
|
184
|
+
webFetch,
|
|
185
|
+
listTools,
|
|
186
|
+
findFile,
|
|
187
|
+
findDir,
|
|
188
|
+
askUser,
|
|
189
|
+
askConfirm
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Dispatches a tool call to the appropriate implementation.
|
|
194
|
+
* @param {string} name Tool name.
|
|
195
|
+
* @param {Object} args Tool arguments.
|
|
196
|
+
* @returns {Promise<string>} Tool execution result.
|
|
197
|
+
*/
|
|
198
|
+
export async function dispatchTool(name, args) {
|
|
199
|
+
const tool = toolImplementations[name];
|
|
200
|
+
if (!tool) {
|
|
201
|
+
return `Error: Tool '${name}' not found.`;
|
|
202
|
+
}
|
|
203
|
+
return await tool(args);
|
|
204
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a detailed list of all available tools, their purpose, and usage guidelines.
|
|
3
|
+
* @returns {Promise<string>} A formatted string containing tool documentation.
|
|
4
|
+
*/
|
|
5
|
+
export default async function listTools() {
|
|
6
|
+
const tools = [
|
|
7
|
+
{
|
|
8
|
+
name: "readFile",
|
|
9
|
+
usage: "readFile({ filePath: 'src/main.js' })",
|
|
10
|
+
description: "Reads the entire content of a file. Use this for small to medium files (<1MB) when you need full context.",
|
|
11
|
+
whenToUse: "When you need to analyze a specific file's logic or structure completely."
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: "readDir",
|
|
15
|
+
usage: "readDir({ dirPath: 'src/' })",
|
|
16
|
+
description: "Lists all files and directories in a given path, indicating their types.",
|
|
17
|
+
whenToUse: "To explore the contents of a specific folder without generating a full tree."
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: "projectTree",
|
|
21
|
+
usage: "projectTree({ dirPath: '.' })",
|
|
22
|
+
description: "Generates a visual directory tree of the project while respecting .gitignore rules.",
|
|
23
|
+
whenToUse: "To get a high-level overview of the project structure and find where specific modules are located."
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "readFileChunk",
|
|
27
|
+
usage: "readFileChunk({ filePath: 'large-file.log', startLine: 100, endLine: 200 })",
|
|
28
|
+
description: "Reads a specific range of lines from a file.",
|
|
29
|
+
whenToUse: "Essential for very large files or when you only need a specific snippet/function from a file."
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "searchText",
|
|
33
|
+
usage: "searchText({ pattern: 'TODO', path: 'src/', recursive: true })",
|
|
34
|
+
description: "Performs a regex search for text across files and directories. Respects .gitignore.",
|
|
35
|
+
whenToUse: "To find variable usages, search for specific strings, or locate technical debt across the codebase."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "webFetch",
|
|
39
|
+
usage: "webFetch({ url: 'https://docs.example.com' })",
|
|
40
|
+
description: "Fetches and extracts clean text from a web URL, removing HTML clutter.",
|
|
41
|
+
whenToUse: "To gather information from online documentation, API references, or external technical articles."
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "findFile",
|
|
45
|
+
usage: "findFile({ pattern: 'config.*\\\\.js$', dirPath: 'src/', limit: 10 })",
|
|
46
|
+
description: "Finds files by name using regex. Supports recursion and respects .gitignore. Max 15 results.",
|
|
47
|
+
whenToUse: "When you know the name or part of the name of a file but don't know its exact location."
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "findDir",
|
|
51
|
+
usage: "findDir({ pattern: 'models', recursive: true })",
|
|
52
|
+
description: "Finds directories by name using regex. Supports recursion and respects .gitignore. Max 15 results.",
|
|
53
|
+
whenToUse: "To locate specific modules or folder structures within the project."
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "listTools",
|
|
57
|
+
usage: "listTools({})",
|
|
58
|
+
description: "Lists all tools available to the AI with detailed descriptions and usage examples.",
|
|
59
|
+
whenToUse: "Use this if you are unsure which tool is best suited for the current task."
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "askUser",
|
|
63
|
+
usage: "askUser({ question: 'What is the API endpoint for this service?' })",
|
|
64
|
+
description: "Asks the user a specific question via the terminal and waits for a text response.",
|
|
65
|
+
whenToUse: "When you need specific information, clarification, or feedback from the user that cannot be found in the codebase."
|
|
66
|
+
}
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
let output = "AVAILABLE TOOLS:\n";
|
|
70
|
+
|
|
71
|
+
tools.forEach(tool => {
|
|
72
|
+
output += `[${tool.name}]\n`;
|
|
73
|
+
output += `Desc:${tool.description}\n`;
|
|
74
|
+
output += `Use:${tool.whenToUse}\n`;
|
|
75
|
+
output += `Ex:${tool.usage}\n`;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return output.trim();
|
|
79
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getIgnorePatterns, shouldIgnore } from '../../utils/fsUtils.js';
|
|
4
|
+
|
|
5
|
+
const MAX_DEPTH = 10;
|
|
6
|
+
const MAX_ITEMS = 500;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generates a robust, visual tree of the project structure.
|
|
10
|
+
* @param {Object} args Arguments.
|
|
11
|
+
* @param {string} args.dirPath The root directory to start from.
|
|
12
|
+
* @returns {Promise<string>} Tree-like string representation.
|
|
13
|
+
*/
|
|
14
|
+
export default async function projectTree({ dirPath }) {
|
|
15
|
+
let itemCount = 0;
|
|
16
|
+
let isTruncated = false;
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const absolutePath = path.resolve(dirPath);
|
|
20
|
+
const stats = await fs.stat(absolutePath);
|
|
21
|
+
|
|
22
|
+
if (!stats.isDirectory()) {
|
|
23
|
+
return `Error: '${dirPath}' is not a directory.`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ignoreList = await getIgnorePatterns();
|
|
27
|
+
|
|
28
|
+
async function buildTree(currentDir, depth = 0, currentPrefix = '') {
|
|
29
|
+
if (depth > MAX_DEPTH || itemCount >= MAX_ITEMS) {
|
|
30
|
+
if (itemCount >= MAX_ITEMS) isTruncated = true;
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let items;
|
|
35
|
+
try {
|
|
36
|
+
items = await fs.readdir(currentDir, { withFileTypes: true });
|
|
37
|
+
} catch (err) {
|
|
38
|
+
if (err.code === 'EACCES') return `${currentPrefix}└[Permission Denied]\n`;
|
|
39
|
+
return `${currentPrefix}└[Error: ${err.code}]\n`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const filteredItems = items
|
|
43
|
+
.filter(item => !shouldIgnore(item.name, ignoreList))
|
|
44
|
+
.sort((a, b) => {
|
|
45
|
+
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
46
|
+
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
47
|
+
return a.name.localeCompare(b.name);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
let result = '';
|
|
51
|
+
for (let i = 0; i < filteredItems.length; i++) {
|
|
52
|
+
if (itemCount >= MAX_ITEMS) {
|
|
53
|
+
isTruncated = true;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const item = filteredItems[i];
|
|
58
|
+
const isLast = i === filteredItems.length - 1 || itemCount + 1 >= MAX_ITEMS;
|
|
59
|
+
const marker = isLast ? '└' : '├';
|
|
60
|
+
const childPrefix = isLast ? ' ' : '│';
|
|
61
|
+
|
|
62
|
+
result += `${currentPrefix}${marker}${item.name}${item.isDirectory() ? '/' : ''}\n`;
|
|
63
|
+
itemCount++;
|
|
64
|
+
|
|
65
|
+
if (item.isDirectory()) {
|
|
66
|
+
const fullPath = path.join(currentDir, item.name);
|
|
67
|
+
result += await buildTree(fullPath, depth + 1, currentPrefix + childPrefix);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const rootName = path.basename(absolutePath) || absolutePath;
|
|
74
|
+
const tree = await buildTree(absolutePath);
|
|
75
|
+
|
|
76
|
+
let finalOutput = `${rootName}/\n${tree || '└(empty)'}`;
|
|
77
|
+
finalOutput = finalOutput.trimEnd();
|
|
78
|
+
|
|
79
|
+
if (isTruncated) {
|
|
80
|
+
finalOutput += `\n[Warning: Truncated at ${MAX_ITEMS} items]`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return finalOutput;
|
|
84
|
+
|
|
85
|
+
} catch (err) {
|
|
86
|
+
if (err.code === 'ENOENT') return `Error: Directory not found at '${dirPath}'.`;
|
|
87
|
+
return `Error generating tree: ${err.message}`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import { getIgnorePatterns, shouldIgnore } from '../../utils/fsUtils.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Implementation of the readDir tool.
|
|
6
|
+
* @param {Object} args Arguments from the model.
|
|
7
|
+
* @param {string} args.dirPath Path to the directory.
|
|
8
|
+
* @returns {Promise<string>} List of files and folders or error message.
|
|
9
|
+
*/
|
|
10
|
+
export default async function readDir({ dirPath }) {
|
|
11
|
+
try {
|
|
12
|
+
const ignoreList = await getIgnorePatterns();
|
|
13
|
+
const items = await fs.readdir(dirPath, { withFileTypes: true });
|
|
14
|
+
|
|
15
|
+
const formattedItems = items
|
|
16
|
+
.filter(item => !shouldIgnore(item.name, ignoreList))
|
|
17
|
+
.map(item => {
|
|
18
|
+
const type = item.isDirectory() ? '[D]' : '[F]';
|
|
19
|
+
return `${type}${item.name}`;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (formattedItems.length === 0) {
|
|
23
|
+
return `Directory '${dirPath}' is empty (or all items are ignored).`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return formattedItems.join('\n');
|
|
27
|
+
} catch (err) {
|
|
28
|
+
if (err.code === 'ENOENT') return `Error: Directory not found at '${dirPath}'.`;
|
|
29
|
+
if (err.code === 'ENOTDIR') return `Error: '${dirPath}' is not a directory.`;
|
|
30
|
+
return `Error reading directory: ${err.message}`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import { Buffer } from 'buffer';
|
|
3
|
+
|
|
4
|
+
const MAX_FILE_SIZE = 1024 * 1024; // 1MB limit
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Implementation of the readFile tool.
|
|
8
|
+
* @param {Object} args Arguments from the model.
|
|
9
|
+
* @param {string} args.filePath Path to the file.
|
|
10
|
+
* @returns {Promise<string>} File content or error message.
|
|
11
|
+
*/
|
|
12
|
+
export default async function readFile({ filePath }) {
|
|
13
|
+
try {
|
|
14
|
+
const stats = await fs.stat(filePath);
|
|
15
|
+
|
|
16
|
+
if (!stats.isFile()) {
|
|
17
|
+
return `Error: '${filePath}' is not a file.`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
21
|
+
return `Error: File is too large (${(stats.size / 1024 / 1024).toFixed(1)}MB). Maximum allowed size is 1MB. Use 'readFileChunk' to read specific parts of this file.`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Binary check: read first 1KB and look for null bytes
|
|
25
|
+
const handle = await fs.open(filePath, 'r');
|
|
26
|
+
const { bytesRead, buffer } = await handle.read(Buffer.alloc(1024), 0, 1024, 0);
|
|
27
|
+
await handle.close();
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i < bytesRead; i++) {
|
|
30
|
+
if (buffer[i] === 0) {
|
|
31
|
+
return `Error: '${filePath}' appears to be a binary file. Reading binary files is not supported.`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
36
|
+
return content;
|
|
37
|
+
} catch (err) {
|
|
38
|
+
if (err.code === 'ENOENT') return `Error: File not found at '${filePath}'.`;
|
|
39
|
+
return `Error reading file: ${err.message}`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import { Buffer } from 'buffer';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Implementation of the readFileChunk tool.
|
|
6
|
+
* @param {Object} args Arguments from the model.
|
|
7
|
+
* @param {string} args.filePath Path to the file.
|
|
8
|
+
* @param {number} args.startLine The 1-based start line.
|
|
9
|
+
* @param {number} args.endLine The 1-based end line (inclusive).
|
|
10
|
+
* @returns {Promise<string>} File chunk or error message.
|
|
11
|
+
*/
|
|
12
|
+
export default async function readFileChunk({ filePath, startLine, endLine }) {
|
|
13
|
+
try {
|
|
14
|
+
const stats = await fs.stat(filePath);
|
|
15
|
+
if (!stats.isFile()) return `Error: '${filePath}' is not a file.`;
|
|
16
|
+
|
|
17
|
+
// Binary check: read first 1KB and look for null bytes
|
|
18
|
+
const handle = await fs.open(filePath, 'r');
|
|
19
|
+
const { bytesRead, buffer } = await handle.read(Buffer.alloc(1024), 0, 1024, 0);
|
|
20
|
+
await handle.close();
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < bytesRead; i++) {
|
|
23
|
+
if (buffer[i] === 0) return `Error: '${filePath}' appears to be a binary file. Reading binary files is not supported.`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const data = await fs.readFile(filePath, 'utf8');
|
|
27
|
+
const lines = data.split('\n');
|
|
28
|
+
|
|
29
|
+
// Boundary validation
|
|
30
|
+
const totalLines = lines.length;
|
|
31
|
+
const actualStart = Math.max(1, startLine);
|
|
32
|
+
const actualEnd = Math.min(totalLines, endLine);
|
|
33
|
+
|
|
34
|
+
if (actualStart > totalLines) {
|
|
35
|
+
return `Error: startLine (${startLine}) is beyond the end of the file (Total lines: ${totalLines}).`;
|
|
36
|
+
}
|
|
37
|
+
if (actualStart > actualEnd) {
|
|
38
|
+
return `Error: startLine (${startLine}) is greater than endLine (${endLine}).`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const chunk = lines.slice(actualStart - 1, actualEnd);
|
|
42
|
+
const header = `--- ${filePath} (Lines ${actualStart}-${actualEnd} of ${totalLines}) ---\n`;
|
|
43
|
+
return header + chunk.join('\n');
|
|
44
|
+
} catch (err) {
|
|
45
|
+
if (err.code === 'ENOENT') return `Error: File not found at '${filePath}'.`;
|
|
46
|
+
return `Error reading file chunk: ${err.message}`;
|
|
47
|
+
}
|
|
48
|
+
}
|