@lowdefy/ai-utils 0.0.0-experimental-20260414134741
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/LICENSE +201 -0
- package/dist/AISDKAgent.js +24 -0
- package/dist/AISDKAgentSchema.js +357 -0
- package/dist/agents.js +15 -0
- package/dist/buildAgentTools.js +268 -0
- package/dist/buildPrepareStep.js +42 -0
- package/dist/fileSystem/listFiles.js +62 -0
- package/dist/fileSystem/readFile.js +36 -0
- package/dist/fileSystem/resolvePath.js +26 -0
- package/dist/fileSystem/searchFiles.js +69 -0
- package/dist/fileSystem/statFile.js +30 -0
- package/dist/handleAgentChat.js +268 -0
- package/dist/index.js +15 -0
- package/dist/types.js +19 -0
- package/package.json +35 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { ToolLoopAgent, tool, jsonSchema, stepCountIs } from 'ai';
|
|
16
|
+
import { createMCPClient } from '@ai-sdk/mcp';
|
|
17
|
+
import { Experimental_StdioMCPTransport } from '@ai-sdk/mcp/mcp-stdio';
|
|
18
|
+
import { serializer } from '@lowdefy/helpers';
|
|
19
|
+
import listFiles from './fileSystem/listFiles.js';
|
|
20
|
+
import readFile from './fileSystem/readFile.js';
|
|
21
|
+
import searchFiles from './fileSystem/searchFiles.js';
|
|
22
|
+
import statFile from './fileSystem/statFile.js';
|
|
23
|
+
// Build artifacts contain serializer markers (~k, ~r, ~l) as non-enumerable
|
|
24
|
+
// properties and ~arr wrappers for arrays. JSON.parse(JSON.stringify(obj))
|
|
25
|
+
// strips non-enumerable props and produces a clean JSON Schema for the AI SDK.
|
|
26
|
+
function cleanBuildArtifact(obj) {
|
|
27
|
+
return JSON.parse(JSON.stringify(obj, (key, value)=>{
|
|
28
|
+
if (key.startsWith('~')) return undefined;
|
|
29
|
+
return value;
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
async function buildAgentTools({ agent, context, depth = 0 }) {
|
|
33
|
+
const MAX_DEPTH = 5;
|
|
34
|
+
if (depth > MAX_DEPTH) {
|
|
35
|
+
throw new Error(`Sub-agent nesting exceeds maximum depth of ${MAX_DEPTH}.`);
|
|
36
|
+
}
|
|
37
|
+
const tools = {};
|
|
38
|
+
const mcpClients = [];
|
|
39
|
+
// Build endpoint tools
|
|
40
|
+
for (const toolConfig of agent.tools ?? []){
|
|
41
|
+
const { endpointId, confirm } = toolConfig;
|
|
42
|
+
const endpointConfig = await context.getEndpointConfig({
|
|
43
|
+
endpointId
|
|
44
|
+
});
|
|
45
|
+
tools[endpointId] = tool({
|
|
46
|
+
description: endpointConfig.description,
|
|
47
|
+
inputSchema: jsonSchema(cleanBuildArtifact(endpointConfig.payloadSchema)),
|
|
48
|
+
...confirm ? {
|
|
49
|
+
needsApproval: true
|
|
50
|
+
} : {},
|
|
51
|
+
execute: async (input, { abortSignal } = {})=>{
|
|
52
|
+
const result = await context.callEndpoint(endpointId, {
|
|
53
|
+
payload: input,
|
|
54
|
+
abortSignal
|
|
55
|
+
});
|
|
56
|
+
if (!result.success) {
|
|
57
|
+
const err = serializer.deserialize(result.error);
|
|
58
|
+
throw new Error(err?.message ?? 'Endpoint execution failed');
|
|
59
|
+
}
|
|
60
|
+
return cleanBuildArtifact(result.response);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
// Build MCP clients and merge their tools
|
|
65
|
+
for (const mcpSource of agent.mcp ?? []){
|
|
66
|
+
const evaluatedSource = context.evaluateOperators(mcpSource);
|
|
67
|
+
try {
|
|
68
|
+
let transport;
|
|
69
|
+
if (evaluatedSource.transport === 'stdio') {
|
|
70
|
+
transport = new Experimental_StdioMCPTransport({
|
|
71
|
+
command: evaluatedSource.command,
|
|
72
|
+
args: evaluatedSource.args,
|
|
73
|
+
env: {
|
|
74
|
+
...process.env,
|
|
75
|
+
...evaluatedSource.env ?? {}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
} else {
|
|
79
|
+
transport = {
|
|
80
|
+
type: evaluatedSource.transport ?? 'http',
|
|
81
|
+
url: evaluatedSource.url,
|
|
82
|
+
...evaluatedSource.headers ? {
|
|
83
|
+
headers: evaluatedSource.headers
|
|
84
|
+
} : {}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const client = await createMCPClient({
|
|
88
|
+
transport
|
|
89
|
+
});
|
|
90
|
+
mcpClients.push({
|
|
91
|
+
client,
|
|
92
|
+
source: evaluatedSource
|
|
93
|
+
});
|
|
94
|
+
} catch (err) {
|
|
95
|
+
const label = evaluatedSource.transport === 'stdio' ? evaluatedSource.command : evaluatedSource.url;
|
|
96
|
+
console.warn(`MCP server "${label}" unreachable: ${err.message}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Merge MCP tools with endpoint tools
|
|
100
|
+
for (const { client, source } of mcpClients){
|
|
101
|
+
try {
|
|
102
|
+
const mcpTools = await client.tools();
|
|
103
|
+
for (const [name, mcpTool] of Object.entries(mcpTools)){
|
|
104
|
+
if (tools[name]) {
|
|
105
|
+
console.warn(`MCP tool "${name}" from ${source.url ?? source.command} conflicts with endpoint tool — skipped.`);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (source.confirm) {
|
|
109
|
+
tools[name] = {
|
|
110
|
+
...mcpTool,
|
|
111
|
+
needsApproval: true
|
|
112
|
+
};
|
|
113
|
+
} else {
|
|
114
|
+
tools[name] = mcpTool;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch (err) {
|
|
118
|
+
const label = source.transport === 'stdio' ? source.command : source.url;
|
|
119
|
+
console.warn(`MCP server "${label}" tool listing failed: ${err.message}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Build sub-agent tools
|
|
123
|
+
for (const subAgentRef of agent.agents ?? []){
|
|
124
|
+
const subAgentConfig = await context.getAgentConfig({
|
|
125
|
+
agentId: subAgentRef.agentId
|
|
126
|
+
});
|
|
127
|
+
const subConnection = await context.getConnectionForAgent({
|
|
128
|
+
agentConfig: subAgentConfig
|
|
129
|
+
});
|
|
130
|
+
subAgentConfig.mcp = await context.resolveMcpSources({
|
|
131
|
+
agentConfig: subAgentConfig
|
|
132
|
+
});
|
|
133
|
+
// Recursively build sub-agent's tools
|
|
134
|
+
const { tools: subTools, mcpClients: subMcpClients } = await buildAgentTools({
|
|
135
|
+
agent: subAgentConfig,
|
|
136
|
+
context,
|
|
137
|
+
depth: depth + 1
|
|
138
|
+
});
|
|
139
|
+
const subModel = subConnection.provider(subAgentConfig.properties.model);
|
|
140
|
+
const subAgent = new ToolLoopAgent({
|
|
141
|
+
model: subModel,
|
|
142
|
+
instructions: subAgentConfig.properties.instructions,
|
|
143
|
+
tools: subTools,
|
|
144
|
+
stopWhen: stepCountIs(subAgentConfig.properties.maxSteps ?? 10),
|
|
145
|
+
maxOutputTokens: subAgentConfig.properties.maxOutputTokens,
|
|
146
|
+
temperature: subAgentConfig.properties.temperature,
|
|
147
|
+
toolChoice: subAgentConfig.properties.toolChoice ?? 'auto',
|
|
148
|
+
providerOptions: subAgentConfig.properties.providerOptions
|
|
149
|
+
});
|
|
150
|
+
const description = subAgentRef.description ?? `Delegate task to the ${subAgentRef.agentId} agent`;
|
|
151
|
+
const inputSchema = subAgentRef.inputSchema ? jsonSchema(subAgentRef.inputSchema) : jsonSchema({
|
|
152
|
+
type: 'object',
|
|
153
|
+
properties: {
|
|
154
|
+
task: {
|
|
155
|
+
type: 'string',
|
|
156
|
+
description: 'The task to delegate'
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
required: [
|
|
160
|
+
'task'
|
|
161
|
+
]
|
|
162
|
+
});
|
|
163
|
+
tools[subAgentRef.agentId] = tool({
|
|
164
|
+
description,
|
|
165
|
+
inputSchema,
|
|
166
|
+
execute: async (input, { abortSignal })=>{
|
|
167
|
+
const prompt = input.task ?? JSON.stringify(input);
|
|
168
|
+
const result = await subAgent.generate({
|
|
169
|
+
prompt,
|
|
170
|
+
abortSignal
|
|
171
|
+
});
|
|
172
|
+
// Cleanup sub-agent's MCP clients
|
|
173
|
+
await Promise.all(subMcpClients.map(({ client })=>client.close().catch(()=>{})));
|
|
174
|
+
return {
|
|
175
|
+
_subAgent: true,
|
|
176
|
+
agentId: subAgentRef.agentId,
|
|
177
|
+
text: result.text
|
|
178
|
+
};
|
|
179
|
+
},
|
|
180
|
+
toModelOutput: ({ output })=>({
|
|
181
|
+
type: 'text',
|
|
182
|
+
value: output.text ?? String(output)
|
|
183
|
+
})
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
// Build fileSystem tools
|
|
187
|
+
if (agent.properties?.fileSystem) {
|
|
188
|
+
const { basePath } = agent.properties.fileSystem;
|
|
189
|
+
tools['read-file'] = tool({
|
|
190
|
+
description: 'Read a file by path. Use list-files or search-files first to discover available paths.',
|
|
191
|
+
inputSchema: jsonSchema({
|
|
192
|
+
type: 'object',
|
|
193
|
+
properties: {
|
|
194
|
+
path: {
|
|
195
|
+
type: 'string',
|
|
196
|
+
description: 'File path relative to the base directory.'
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
required: [
|
|
200
|
+
'path'
|
|
201
|
+
]
|
|
202
|
+
}),
|
|
203
|
+
execute: async ({ path })=>readFile(basePath, {
|
|
204
|
+
path
|
|
205
|
+
})
|
|
206
|
+
});
|
|
207
|
+
tools['list-files'] = tool({
|
|
208
|
+
description: 'List files and directories. Supports optional glob patterns for filtering.',
|
|
209
|
+
inputSchema: jsonSchema({
|
|
210
|
+
type: 'object',
|
|
211
|
+
properties: {
|
|
212
|
+
path: {
|
|
213
|
+
type: 'string',
|
|
214
|
+
description: 'Directory path to list. Omit for root.'
|
|
215
|
+
},
|
|
216
|
+
glob: {
|
|
217
|
+
type: 'string',
|
|
218
|
+
description: 'Glob pattern to filter results, e.g. "**/*.md".'
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}),
|
|
222
|
+
execute: async (params)=>listFiles(basePath, params)
|
|
223
|
+
});
|
|
224
|
+
tools['search-files'] = tool({
|
|
225
|
+
description: 'Search for text across files. Returns matching files with line numbers and context.',
|
|
226
|
+
inputSchema: jsonSchema({
|
|
227
|
+
type: 'object',
|
|
228
|
+
properties: {
|
|
229
|
+
query: {
|
|
230
|
+
type: 'string',
|
|
231
|
+
description: 'Text to search for.'
|
|
232
|
+
},
|
|
233
|
+
glob: {
|
|
234
|
+
type: 'string',
|
|
235
|
+
description: 'Glob pattern to limit which files are searched.'
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
required: [
|
|
239
|
+
'query'
|
|
240
|
+
]
|
|
241
|
+
}),
|
|
242
|
+
execute: async (params)=>searchFiles(basePath, params)
|
|
243
|
+
});
|
|
244
|
+
tools['stat-file'] = tool({
|
|
245
|
+
description: 'Get metadata for a file or directory (size, type, modified date).',
|
|
246
|
+
inputSchema: jsonSchema({
|
|
247
|
+
type: 'object',
|
|
248
|
+
properties: {
|
|
249
|
+
path: {
|
|
250
|
+
type: 'string',
|
|
251
|
+
description: 'File or directory path relative to the base directory.'
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
required: [
|
|
255
|
+
'path'
|
|
256
|
+
]
|
|
257
|
+
}),
|
|
258
|
+
execute: async ({ path })=>statFile(basePath, {
|
|
259
|
+
path
|
|
260
|
+
})
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
tools,
|
|
265
|
+
mcpClients
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
export default buildAgentTools;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ function matchesRule(rule, stepNumber) {
|
|
16
|
+
if (rule.steps) {
|
|
17
|
+
return rule.steps.includes(stepNumber);
|
|
18
|
+
}
|
|
19
|
+
if (rule.from != null) {
|
|
20
|
+
const fromOk = stepNumber >= rule.from;
|
|
21
|
+
const toOk = rule.to == null || stepNumber <= rule.to;
|
|
22
|
+
return fromOk && toOk;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
function buildPrepareStep(rules) {
|
|
27
|
+
if (!rules || rules.length === 0) return undefined;
|
|
28
|
+
return async function prepareStep({ stepNumber }) {
|
|
29
|
+
for (const rule of rules){
|
|
30
|
+
if (matchesRule(rule, stepNumber)) {
|
|
31
|
+
const config = {};
|
|
32
|
+
if (rule.activeTools) config.activeTools = rule.activeTools;
|
|
33
|
+
if (rule.toolChoice) config.toolChoice = rule.toolChoice;
|
|
34
|
+
if (rule.maxOutputTokens) config.maxOutputTokens = rule.maxOutputTokens;
|
|
35
|
+
if (rule.temperature != null) config.temperature = rule.temperature;
|
|
36
|
+
return config;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return {};
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export default buildPrepareStep;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import fs from 'fs/promises';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import { glob } from 'glob';
|
|
18
|
+
import resolvePath from './resolvePath.js';
|
|
19
|
+
async function listFiles(basePath, { path: dirPath, glob: globPattern } = {}) {
|
|
20
|
+
const absoluteBase = path.resolve(basePath);
|
|
21
|
+
const resolved = resolvePath(basePath, dirPath);
|
|
22
|
+
if (globPattern) {
|
|
23
|
+
const matches = await glob(globPattern, {
|
|
24
|
+
cwd: resolved,
|
|
25
|
+
dot: true
|
|
26
|
+
});
|
|
27
|
+
const safeMatches = matches.filter((match)=>{
|
|
28
|
+
const abs = path.resolve(resolved, match);
|
|
29
|
+
return abs === absoluteBase || abs.startsWith(absoluteBase + path.sep);
|
|
30
|
+
});
|
|
31
|
+
const results = [];
|
|
32
|
+
for (const match of safeMatches.sort()){
|
|
33
|
+
const absoluteMatch = path.resolve(resolved, match);
|
|
34
|
+
const relativePath = path.relative(absoluteBase, absoluteMatch);
|
|
35
|
+
const stats = await fs.stat(absoluteMatch);
|
|
36
|
+
results.push({
|
|
37
|
+
name: path.basename(match),
|
|
38
|
+
path: relativePath,
|
|
39
|
+
type: stats.isDirectory() ? 'directory' : 'file',
|
|
40
|
+
size: stats.size
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return results;
|
|
44
|
+
}
|
|
45
|
+
const entries = await fs.readdir(resolved, {
|
|
46
|
+
withFileTypes: true
|
|
47
|
+
});
|
|
48
|
+
const results = [];
|
|
49
|
+
for (const entry of entries.sort((a, b)=>a.name.localeCompare(b.name))){
|
|
50
|
+
const absoluteEntry = path.join(resolved, entry.name);
|
|
51
|
+
const relativePath = path.relative(absoluteBase, absoluteEntry);
|
|
52
|
+
const stats = await fs.stat(absoluteEntry);
|
|
53
|
+
results.push({
|
|
54
|
+
name: entry.name,
|
|
55
|
+
path: relativePath,
|
|
56
|
+
type: entry.isDirectory() ? 'directory' : 'file',
|
|
57
|
+
size: stats.size
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return results;
|
|
61
|
+
}
|
|
62
|
+
export default listFiles;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import fs from 'fs/promises';
|
|
16
|
+
import resolvePath from './resolvePath.js';
|
|
17
|
+
const MAX_FILE_SIZE = 512 * 1024; // 512KB
|
|
18
|
+
async function readFile(basePath, { path: filePath }) {
|
|
19
|
+
const resolved = resolvePath(basePath, filePath);
|
|
20
|
+
const stats = await fs.stat(resolved);
|
|
21
|
+
if (stats.size <= MAX_FILE_SIZE) {
|
|
22
|
+
return await fs.readFile(resolved, 'utf-8');
|
|
23
|
+
}
|
|
24
|
+
const handle = await fs.open(resolved, 'r');
|
|
25
|
+
try {
|
|
26
|
+
const buffer = Buffer.alloc(MAX_FILE_SIZE);
|
|
27
|
+
await handle.read(buffer, 0, MAX_FILE_SIZE, 0);
|
|
28
|
+
const content = buffer.toString('utf-8');
|
|
29
|
+
const shownKB = Math.round(MAX_FILE_SIZE / 1024);
|
|
30
|
+
const totalKB = Math.round(stats.size / 1024);
|
|
31
|
+
return `${content}\n\n[File truncated — showing first ${shownKB}KB of ${totalKB}KB. Use search-files to find specific content.]`;
|
|
32
|
+
} finally{
|
|
33
|
+
await handle.close();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export default readFile;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import path from 'path';
|
|
16
|
+
function resolvePath(basePath, requestPath) {
|
|
17
|
+
const normalizedRequest = (requestPath ?? '').replace(/^\/+/, '');
|
|
18
|
+
const absoluteBase = path.resolve(basePath);
|
|
19
|
+
const absoluteTarget = path.resolve(absoluteBase, normalizedRequest || '.');
|
|
20
|
+
const baseWithSep = absoluteBase + path.sep;
|
|
21
|
+
if (absoluteTarget !== absoluteBase && !absoluteTarget.startsWith(baseWithSep)) {
|
|
22
|
+
throw new Error(`Path "${requestPath}" resolves outside the base directory.`);
|
|
23
|
+
}
|
|
24
|
+
return absoluteTarget;
|
|
25
|
+
}
|
|
26
|
+
export default resolvePath;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import fs from 'fs/promises';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import { glob } from 'glob';
|
|
18
|
+
const MAX_FILE_SIZE = 1024 * 1024; // 1MB
|
|
19
|
+
const MAX_TOTAL_MATCHES = 200;
|
|
20
|
+
async function searchFiles(basePath, { query, glob: globPattern }) {
|
|
21
|
+
const absoluteBase = path.resolve(basePath);
|
|
22
|
+
const baseWithSep = absoluteBase + path.sep;
|
|
23
|
+
const pattern = globPattern ?? '**/*';
|
|
24
|
+
const files = await glob(pattern, {
|
|
25
|
+
cwd: absoluteBase,
|
|
26
|
+
nodir: true,
|
|
27
|
+
dot: true,
|
|
28
|
+
absolute: true
|
|
29
|
+
});
|
|
30
|
+
const safeFiles = files.filter((file)=>file.startsWith(baseWithSep));
|
|
31
|
+
const results = [];
|
|
32
|
+
let totalMatches = 0;
|
|
33
|
+
const queryLower = query.toLowerCase();
|
|
34
|
+
for (const file of safeFiles.sort()){
|
|
35
|
+
if (totalMatches >= MAX_TOTAL_MATCHES) break;
|
|
36
|
+
const stats = await fs.stat(file);
|
|
37
|
+
if (stats.size > MAX_FILE_SIZE) continue;
|
|
38
|
+
let content;
|
|
39
|
+
try {
|
|
40
|
+
content = await fs.readFile(file, 'utf-8');
|
|
41
|
+
} catch {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const lines = content.split('\n');
|
|
45
|
+
const matches = [];
|
|
46
|
+
for(let i = 0; i < lines.length; i++){
|
|
47
|
+
if (totalMatches >= MAX_TOTAL_MATCHES) break;
|
|
48
|
+
if (lines[i].toLowerCase().includes(queryLower)) {
|
|
49
|
+
matches.push({
|
|
50
|
+
lineNumber: i + 1,
|
|
51
|
+
line: lines[i],
|
|
52
|
+
context: {
|
|
53
|
+
before: i > 0 ? lines[i - 1] : null,
|
|
54
|
+
after: i < lines.length - 1 ? lines[i + 1] : null
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
totalMatches += 1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (matches.length > 0) {
|
|
61
|
+
results.push({
|
|
62
|
+
path: path.relative(absoluteBase, file),
|
|
63
|
+
matches
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return results;
|
|
68
|
+
}
|
|
69
|
+
export default searchFiles;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import fs from 'fs/promises';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import resolvePath from './resolvePath.js';
|
|
18
|
+
async function statFile(basePath, { path: filePath }) {
|
|
19
|
+
const resolved = resolvePath(basePath, filePath);
|
|
20
|
+
const stats = await fs.stat(resolved);
|
|
21
|
+
return {
|
|
22
|
+
name: path.basename(resolved),
|
|
23
|
+
path: filePath,
|
|
24
|
+
size: stats.size,
|
|
25
|
+
type: stats.isDirectory() ? 'directory' : 'file',
|
|
26
|
+
modified: stats.mtime.toISOString(),
|
|
27
|
+
created: stats.birthtime.toISOString()
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export default statFile;
|