actoviq-agent-sdk 0.2.0 → 0.3.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-zh.md +19 -3
- package/README.md +19 -3
- package/bin/actoviq-interactive-agent.js +5 -0
- package/bin/actoviq-link-runtime.js +74 -0
- package/bin/actoviq-react.js +5 -0
- package/dist/src/cli/actoviq-react.d.ts +3 -0
- package/dist/src/cli/actoviq-react.d.ts.map +1 -0
- package/dist/src/cli/actoviq-react.js +277 -0
- package/dist/src/cli/actoviq-react.js.map +1 -0
- package/dist/src/cli/bridge-interactive-agent.d.ts +2 -0
- package/dist/src/cli/bridge-interactive-agent.d.ts.map +1 -0
- package/dist/src/cli/bridge-interactive-agent.js +147 -0
- package/dist/src/cli/bridge-interactive-agent.js.map +1 -0
- package/dist/src/config/resolveRuntimeConfig.d.ts.map +1 -1
- package/dist/src/config/resolveRuntimeConfig.js +13 -16
- package/dist/src/config/resolveRuntimeConfig.js.map +1 -1
- package/dist/src/errors.d.ts +9 -0
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +9 -0
- package/dist/src/errors.js.map +1 -1
- package/dist/src/index.d.ts +20 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +18 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/parity/actoviqBridgeEvents.d.ts +7 -0
- package/dist/src/parity/actoviqBridgeEvents.d.ts.map +1 -0
- package/dist/src/parity/actoviqBridgeEvents.js +133 -0
- package/dist/src/parity/actoviqBridgeEvents.js.map +1 -0
- package/dist/src/parity/actoviqBridgeSdk.d.ts +179 -0
- package/dist/src/parity/actoviqBridgeSdk.d.ts.map +1 -0
- package/dist/src/parity/actoviqBridgeSdk.js +1267 -0
- package/dist/src/parity/actoviqBridgeSdk.js.map +1 -0
- package/dist/src/parity/actoviqTranscripts.d.ts +44 -0
- package/dist/src/parity/actoviqTranscripts.d.ts.map +1 -0
- package/dist/src/parity/actoviqTranscripts.js +282 -0
- package/dist/src/parity/actoviqTranscripts.js.map +1 -0
- package/dist/src/parity/portableSessions.d.ts +35 -0
- package/dist/src/parity/portableSessions.d.ts.map +1 -0
- package/dist/src/parity/portableSessions.js +233 -0
- package/dist/src/parity/portableSessions.js.map +1 -0
- package/dist/src/prompts/systemPrompt.d.ts +15 -0
- package/dist/src/prompts/systemPrompt.d.ts.map +1 -0
- package/dist/src/prompts/systemPrompt.js +73 -0
- package/dist/src/prompts/systemPrompt.js.map +1 -0
- package/dist/src/provider/client.d.ts +1 -0
- package/dist/src/provider/client.d.ts.map +1 -1
- package/dist/src/provider/client.js +6 -5
- package/dist/src/provider/client.js.map +1 -1
- package/dist/src/provider/json-parse.d.ts +7 -0
- package/dist/src/provider/json-parse.d.ts.map +1 -0
- package/dist/src/provider/json-parse.js +69 -0
- package/dist/src/provider/json-parse.js.map +1 -0
- package/dist/src/provider/openai-client.d.ts.map +1 -1
- package/dist/src/provider/openai-client.js +2 -1
- package/dist/src/provider/openai-client.js.map +1 -1
- package/dist/src/provider/openai-model-api.d.ts.map +1 -1
- package/dist/src/provider/openai-model-api.js +2 -7
- package/dist/src/provider/openai-model-api.js.map +1 -1
- package/dist/src/runtime/actoviqApiMicrocompact.d.ts.map +1 -1
- package/dist/src/runtime/actoviqApiMicrocompact.js +10 -17
- package/dist/src/runtime/actoviqApiMicrocompact.js.map +1 -1
- package/dist/src/runtime/actoviqCompact.d.ts.map +1 -1
- package/dist/src/runtime/actoviqCompact.js +53 -1
- package/dist/src/runtime/actoviqCompact.js.map +1 -1
- package/dist/src/runtime/actoviqPermissions.d.ts.map +1 -1
- package/dist/src/runtime/actoviqPermissions.js +120 -54
- package/dist/src/runtime/actoviqPermissions.js.map +1 -1
- package/dist/src/runtime/agentClient.d.ts.map +1 -1
- package/dist/src/runtime/agentClient.js +30 -2
- package/dist/src/runtime/agentClient.js.map +1 -1
- package/dist/src/runtime/conversationEngine.d.ts.map +1 -1
- package/dist/src/runtime/conversationEngine.js +95 -3
- package/dist/src/runtime/conversationEngine.js.map +1 -1
- package/dist/src/runtime/denialTracking.d.ts +10 -0
- package/dist/src/runtime/denialTracking.d.ts.map +1 -0
- package/dist/src/runtime/denialTracking.js +27 -0
- package/dist/src/runtime/denialTracking.js.map +1 -0
- package/dist/src/runtime/safetyChecks.d.ts +12 -0
- package/dist/src/runtime/safetyChecks.d.ts.map +1 -0
- package/dist/src/runtime/safetyChecks.js +62 -0
- package/dist/src/runtime/safetyChecks.js.map +1 -0
- package/dist/src/runtime/tools.d.ts.map +1 -1
- package/dist/src/runtime/tools.js +61 -3
- package/dist/src/runtime/tools.js.map +1 -1
- package/dist/src/scheduling/cron.d.ts +14 -0
- package/dist/src/scheduling/cron.d.ts.map +1 -0
- package/dist/src/scheduling/cron.js +157 -0
- package/dist/src/scheduling/cron.js.map +1 -0
- package/dist/src/scheduling/index.d.ts +3 -0
- package/dist/src/scheduling/index.d.ts.map +1 -0
- package/dist/src/scheduling/index.js +3 -0
- package/dist/src/scheduling/index.js.map +1 -0
- package/dist/src/scheduling/scheduler.d.ts +32 -0
- package/dist/src/scheduling/scheduler.d.ts.map +1 -0
- package/dist/src/scheduling/scheduler.js +196 -0
- package/dist/src/scheduling/scheduler.js.map +1 -0
- package/dist/src/tools/actoviqCoreTools.d.ts +23 -0
- package/dist/src/tools/actoviqCoreTools.d.ts.map +1 -0
- package/dist/src/tools/actoviqCoreTools.js +32 -0
- package/dist/src/tools/actoviqCoreTools.js.map +1 -0
- package/dist/src/tools/actoviqFileTools.d.ts.map +1 -1
- package/dist/src/tools/actoviqFileTools.js +338 -373
- package/dist/src/tools/actoviqFileTools.js.map +1 -1
- package/dist/src/tools/actoviqMiscTools.d.ts +11 -0
- package/dist/src/tools/actoviqMiscTools.d.ts.map +1 -0
- package/dist/src/tools/actoviqMiscTools.js +89 -0
- package/dist/src/tools/actoviqMiscTools.js.map +1 -0
- package/dist/src/tools/actoviqNotebookEdit.d.ts +4 -0
- package/dist/src/tools/actoviqNotebookEdit.d.ts.map +1 -0
- package/dist/src/tools/actoviqNotebookEdit.js +23 -0
- package/dist/src/tools/actoviqNotebookEdit.js.map +1 -0
- package/dist/src/tools/actoviqShellTools.d.ts +5 -0
- package/dist/src/tools/actoviqShellTools.d.ts.map +1 -0
- package/dist/src/tools/actoviqShellTools.js +35 -0
- package/dist/src/tools/actoviqShellTools.js.map +1 -0
- package/dist/src/tools/actoviqTaskTools.d.ts +12 -0
- package/dist/src/tools/actoviqTaskTools.d.ts.map +1 -0
- package/dist/src/tools/actoviqTaskTools.js +108 -0
- package/dist/src/tools/actoviqTaskTools.js.map +1 -0
- package/dist/src/tools/actoviqWebTools.d.ts +15 -0
- package/dist/src/tools/actoviqWebTools.d.ts.map +1 -0
- package/dist/src/tools/actoviqWebTools.js +395 -0
- package/dist/src/tools/actoviqWebTools.js.map +1 -0
- package/dist/src/tools/askUserQuestion/AskUserQuestionTool.d.ts +6 -0
- package/dist/src/tools/askUserQuestion/AskUserQuestionTool.d.ts.map +1 -0
- package/dist/src/tools/askUserQuestion/AskUserQuestionTool.js +43 -0
- package/dist/src/tools/askUserQuestion/AskUserQuestionTool.js.map +1 -0
- package/dist/src/tools/bash/BashTool.d.ts +14 -0
- package/dist/src/tools/bash/BashTool.d.ts.map +1 -0
- package/dist/src/tools/bash/BashTool.js +60 -0
- package/dist/src/tools/bash/BashTool.js.map +1 -0
- package/dist/src/tools/bash/prompt.d.ts +3 -0
- package/dist/src/tools/bash/prompt.d.ts.map +1 -0
- package/dist/src/tools/bash/prompt.js +63 -0
- package/dist/src/tools/bash/prompt.js.map +1 -0
- package/dist/src/tools/prompts/fileEditPrompt.d.ts +3 -0
- package/dist/src/tools/prompts/fileEditPrompt.d.ts.map +1 -0
- package/dist/src/tools/prompts/fileEditPrompt.js +14 -0
- package/dist/src/tools/prompts/fileEditPrompt.js.map +1 -0
- package/dist/src/tools/prompts/fileReadPrompt.d.ts +3 -0
- package/dist/src/tools/prompts/fileReadPrompt.d.ts.map +1 -0
- package/dist/src/tools/prompts/fileReadPrompt.js +18 -0
- package/dist/src/tools/prompts/fileReadPrompt.js.map +1 -0
- package/dist/src/tools/prompts/fileSearchPrompt.d.ts +2 -0
- package/dist/src/tools/prompts/fileSearchPrompt.d.ts.map +1 -0
- package/dist/src/tools/prompts/fileSearchPrompt.js +21 -0
- package/dist/src/tools/prompts/fileSearchPrompt.js.map +1 -0
- package/dist/src/tools/prompts/fileWritePrompt.d.ts +3 -0
- package/dist/src/tools/prompts/fileWritePrompt.d.ts.map +1 -0
- package/dist/src/tools/prompts/fileWritePrompt.js +13 -0
- package/dist/src/tools/prompts/fileWritePrompt.js.map +1 -0
- package/dist/src/tools/prompts/webFetchPrompt.d.ts +3 -0
- package/dist/src/tools/prompts/webFetchPrompt.d.ts.map +1 -0
- package/dist/src/tools/prompts/webFetchPrompt.js +13 -0
- package/dist/src/tools/prompts/webFetchPrompt.js.map +1 -0
- package/dist/src/tools/prompts/webSearchPrompt.d.ts +3 -0
- package/dist/src/tools/prompts/webSearchPrompt.d.ts.map +1 -0
- package/dist/src/tools/prompts/webSearchPrompt.js +17 -0
- package/dist/src/tools/prompts/webSearchPrompt.js.map +1 -0
- package/dist/src/tools/todo/TodoWriteTool.d.ts +6 -0
- package/dist/src/tools/todo/TodoWriteTool.d.ts.map +1 -0
- package/dist/src/tools/todo/TodoWriteTool.js +62 -0
- package/dist/src/tools/todo/TodoWriteTool.js.map +1 -0
- package/dist/src/types.d.ts +309 -2
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js.map +1 -1
- package/package.json +35 -11
- package/vendor/actoviq-runtime/cli.js +54 -0
|
@@ -1,426 +1,391 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Actoviq File Tools — Read, Write, Edit, Glob, Grep
|
|
3
|
+
*
|
|
4
|
+
* Schemas, descriptions, and prompts match Claude Code exactly.
|
|
5
|
+
* Execution logic is adapted for the Actoviq SDK environment.
|
|
6
|
+
*/
|
|
7
|
+
import { readFile, writeFile, mkdir, stat as fsStat } from 'node:fs/promises';
|
|
2
8
|
import os from 'node:os';
|
|
3
9
|
import path from 'node:path';
|
|
4
10
|
import { glob } from 'glob';
|
|
5
11
|
import { z } from 'zod';
|
|
6
12
|
import { ToolExecutionError } from '../errors.js';
|
|
7
13
|
import { tool } from '../runtime/tools.js';
|
|
14
|
+
import { fileReadPrompt, FILE_READ_TOOL_NAME } from './prompts/fileReadPrompt.js';
|
|
15
|
+
import { fileWritePrompt, FILE_WRITE_TOOL_NAME } from './prompts/fileWritePrompt.js';
|
|
16
|
+
import { fileEditPrompt, FILE_EDIT_TOOL_NAME } from './prompts/fileEditPrompt.js';
|
|
17
|
+
import { fileSearchPrompt } from './prompts/fileSearchPrompt.js';
|
|
8
18
|
const DEFAULT_MAX_READ_LINES = 2000;
|
|
9
19
|
const DEFAULT_GLOB_LIMIT = 100;
|
|
10
20
|
const DEFAULT_GREP_LIMIT = 250;
|
|
11
21
|
const BINARY_ZERO_BYTE = 0;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
22
|
+
const PDF_MAX_PAGES = 20;
|
|
23
|
+
const Read = (opts) => tool({
|
|
24
|
+
name: FILE_READ_TOOL_NAME,
|
|
25
|
+
description: 'Reads a file from the local filesystem. You can access any file directly by using this tool.\n' +
|
|
26
|
+
'Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid.\n\n' +
|
|
27
|
+
'Usage:\n' +
|
|
28
|
+
'- The file_path parameter must be an absolute path, not a relative path\n' +
|
|
29
|
+
`- By default, it reads up to ${opts.maxReadLines} lines starting from the beginning of the file\n` +
|
|
30
|
+
'- When you already know which part of the file you need, only read that part.\n' +
|
|
31
|
+
'- Results are returned using cat -n format, with line numbers starting at 1\n' +
|
|
32
|
+
'- This tool can read images (PNG, JPG, etc) and PDF files (.pdf)\n' +
|
|
33
|
+
'- This tool can read Jupyter notebooks (.ipynb files) and returns all cells\n' +
|
|
34
|
+
'- This tool can only read files, not directories.\n' +
|
|
35
|
+
'- You will regularly be asked to read screenshots. ALWAYS use this tool to view the file at the path.\n' +
|
|
36
|
+
'- If you read a file that exists but has empty contents you will receive a system reminder.',
|
|
37
|
+
inputSchema: z.strictObject({
|
|
38
|
+
file_path: z.string().describe('The absolute path to the file to read'),
|
|
39
|
+
offset: z.number().int().nonnegative().optional().describe('The line number to start reading from. Only provide if the file is too large to read at once'),
|
|
40
|
+
limit: z.number().int().positive().optional().describe('The number of lines to read. Only provide if the file is too large to read at once.'),
|
|
41
|
+
pages: z.string().optional().describe(`Page range for PDF files (e.g., "1-5", "3", "10-20"). Only applicable to PDF files. Maximum ${PDF_MAX_PAGES} pages per request.`),
|
|
42
|
+
}),
|
|
43
|
+
isReadOnly: () => true,
|
|
44
|
+
prompt: fileReadPrompt,
|
|
45
|
+
}, async (input) => {
|
|
46
|
+
const resolvedPath = resolvePath(input.file_path, opts.cwd);
|
|
47
|
+
let fileStats;
|
|
48
|
+
try {
|
|
49
|
+
fileStats = await fsStat(resolvedPath);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
throw new ToolExecutionError('Read', `File not found: ${resolvedPath}`);
|
|
53
|
+
}
|
|
54
|
+
if (!fileStats.isFile()) {
|
|
55
|
+
throw new ToolExecutionError('Read', `Path is not a file: ${resolvedPath}`);
|
|
56
|
+
}
|
|
57
|
+
// Handle PDF
|
|
58
|
+
if (resolvedPath.toLowerCase().endsWith('.pdf') && input.pages) {
|
|
59
|
+
throw new ToolExecutionError('Read', 'PDF reading requires pdf-parse dependency. Install it to read PDFs.');
|
|
60
|
+
}
|
|
61
|
+
const buffer = await readFile(resolvedPath);
|
|
62
|
+
if (isProbablyBinary(buffer) && !isImagePath(resolvedPath) && !isPDFPath(resolvedPath)) {
|
|
63
|
+
throw new ToolExecutionError('Read', `Cannot read binary file: ${resolvedPath}`);
|
|
64
|
+
}
|
|
65
|
+
const text = buffer.toString('utf-8');
|
|
66
|
+
opts.readState.set(resolvedPath, { mtimeMs: fileStats.mtimeMs });
|
|
67
|
+
const lines = text.split(/\r?\n/);
|
|
68
|
+
const startLine = input.offset ?? 1;
|
|
69
|
+
const startIndex = Math.max(0, startLine - 1);
|
|
70
|
+
const lineCount = input.limit ?? opts.maxReadLines;
|
|
71
|
+
const selected = lines.slice(startIndex, startIndex + lineCount);
|
|
72
|
+
const content = selected.map((l, i) => `${String(startLine + i).padStart(6, ' ')}\t${l}`).join('\n');
|
|
73
|
+
return {
|
|
74
|
+
type: 'text',
|
|
75
|
+
file: {
|
|
53
76
|
filePath: resolvedPath,
|
|
54
|
-
startLine: startIndex + 1,
|
|
55
|
-
endLine: startIndex + selectedLines.length,
|
|
56
|
-
totalLines: lines.length,
|
|
57
|
-
truncated: startIndex + selectedLines.length < lines.length,
|
|
58
77
|
content,
|
|
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
|
-
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
|
-
|
|
78
|
+
numLines: selected.length,
|
|
79
|
+
startLine,
|
|
80
|
+
totalLines: lines.length,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
// ── Write tool (matches Claude Code FileWriteTool) ───────────────
|
|
85
|
+
const Write = (opts) => tool({
|
|
86
|
+
name: FILE_WRITE_TOOL_NAME,
|
|
87
|
+
description: 'Writes a file to the local filesystem.\n\n' +
|
|
88
|
+
'Usage:\n' +
|
|
89
|
+
'- This tool will overwrite the existing file if there is one at the provided path.\n' +
|
|
90
|
+
`- If this is an existing file, you MUST use the \`${FILE_READ_TOOL_NAME}\` tool first to read the file's contents.\n` +
|
|
91
|
+
'- Prefer the Edit tool for modifying existing files — it only sends the diff. Only use this tool to create new files or for complete rewrites.\n' +
|
|
92
|
+
'- NEVER create documentation files (*.md) or README files unless explicitly requested by the User.\n' +
|
|
93
|
+
'- Only use emojis if the user explicitly requests it.',
|
|
94
|
+
inputSchema: z.strictObject({
|
|
95
|
+
file_path: z.string().describe('The absolute path to the file to write (must be absolute, not relative)'),
|
|
96
|
+
content: z.string().describe('The content to write to the file'),
|
|
97
|
+
}),
|
|
98
|
+
isDestructive: () => true,
|
|
99
|
+
prompt: fileWritePrompt,
|
|
100
|
+
}, async (input) => {
|
|
101
|
+
const resolvedPath = resolvePath(input.file_path, opts.cwd);
|
|
102
|
+
let existing;
|
|
103
|
+
try {
|
|
104
|
+
existing = await fsStat(resolvedPath);
|
|
105
|
+
}
|
|
106
|
+
catch { /* new file */ }
|
|
107
|
+
if (existing?.isFile()) {
|
|
108
|
+
ensurePreviouslyRead(opts.readState, resolvedPath, existing.mtimeMs, 'Write');
|
|
109
|
+
}
|
|
110
|
+
await mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
111
|
+
await writeFile(resolvedPath, input.content, 'utf-8');
|
|
112
|
+
const finalStats = await fsStat(resolvedPath);
|
|
113
|
+
opts.readState.set(resolvedPath, { mtimeMs: finalStats.mtimeMs });
|
|
114
|
+
return {
|
|
115
|
+
type: existing ? 'update' : 'create',
|
|
116
|
+
filePath: resolvedPath,
|
|
117
|
+
content: input.content,
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
// ── Edit tool (matches Claude Code FileEditTool) ─────────────────
|
|
121
|
+
const Edit = (opts) => tool({
|
|
122
|
+
name: FILE_EDIT_TOOL_NAME,
|
|
123
|
+
description: 'Performs exact string replacements in files.\n\n' +
|
|
124
|
+
'Usage:\n' +
|
|
125
|
+
`- You must use your \`${FILE_READ_TOOL_NAME}\` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.\n` +
|
|
126
|
+
'- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: line number + tab. Everything after that is the actual file content to match.\n' +
|
|
127
|
+
'- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\n' +
|
|
128
|
+
'- Only use emojis if the user explicitly requests it.\n' +
|
|
129
|
+
'- The edit will FAIL if `old_string` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use `replace_all` to change every instance of `old_string`.\n' +
|
|
130
|
+
'- Use `replace_all` for replacing and renaming strings across the file.',
|
|
131
|
+
inputSchema: z.strictObject({
|
|
132
|
+
file_path: z.string().describe('The absolute path to the file to modify'),
|
|
133
|
+
old_string: z.string().describe('The text to replace'),
|
|
134
|
+
new_string: z.string().describe('The text to replace it with (must be different from old_string)'),
|
|
135
|
+
replace_all: z.boolean().optional().default(false),
|
|
136
|
+
}),
|
|
137
|
+
isDestructive: () => true,
|
|
138
|
+
prompt: fileEditPrompt,
|
|
139
|
+
}, async (input) => {
|
|
140
|
+
const resolvedPath = resolvePath(input.file_path, opts.cwd);
|
|
141
|
+
let fileStats;
|
|
142
|
+
try {
|
|
143
|
+
fileStats = await fsStat(resolvedPath);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
throw new ToolExecutionError('Edit', `File not found: ${resolvedPath}`);
|
|
147
|
+
}
|
|
148
|
+
if (!fileStats.isFile()) {
|
|
149
|
+
throw new ToolExecutionError('Edit', `Path is not a file: ${resolvedPath}`);
|
|
150
|
+
}
|
|
151
|
+
ensurePreviouslyRead(opts.readState, resolvedPath, fileStats.mtimeMs, 'Edit');
|
|
152
|
+
const originalContent = await readFile(resolvedPath, 'utf-8');
|
|
153
|
+
const occurrences = (originalContent.match(new RegExp(escapeRegex(input.old_string), 'g')) || []).length;
|
|
154
|
+
if (occurrences === 0) {
|
|
155
|
+
throw new ToolExecutionError('Edit', `old_string was not found in ${resolvedPath}`);
|
|
156
|
+
}
|
|
157
|
+
if (!input.replace_all && occurrences > 1) {
|
|
158
|
+
throw new ToolExecutionError('Edit', `old_string matched ${occurrences} locations in ${resolvedPath}. Use replace_all: true or provide a more specific old_string.`);
|
|
159
|
+
}
|
|
160
|
+
const updatedContent = input.replace_all
|
|
161
|
+
? originalContent.split(input.old_string).join(input.new_string)
|
|
162
|
+
: originalContent.replace(input.old_string, input.new_string);
|
|
163
|
+
await writeFile(resolvedPath, updatedContent, 'utf-8');
|
|
164
|
+
const finalStats = await fsStat(resolvedPath);
|
|
165
|
+
opts.readState.set(resolvedPath, { mtimeMs: finalStats.mtimeMs });
|
|
166
|
+
return {
|
|
167
|
+
filePath: resolvedPath,
|
|
168
|
+
replacements: input.replace_all ? occurrences : 1,
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
// ── Glob tool (matches Claude Code GlobTool) ─────────────────────
|
|
172
|
+
const GLOB_TOOL_NAME = 'Glob';
|
|
173
|
+
const defaultGlobExcludes = ['**/.git/**', '**/node_modules/**', '**/.svn/**', '**/.hg/**', '**/.jj/**', '**/.sl/**'];
|
|
174
|
+
const Glob = (opts) => tool({
|
|
175
|
+
name: GLOB_TOOL_NAME,
|
|
176
|
+
description: '- Fast file pattern matching tool that works with any codebase size\n' +
|
|
177
|
+
'- Supports glob patterns like "**/*.js" or "src/**/*.ts"\n' +
|
|
178
|
+
'- Returns matching file paths sorted by modification time\n' +
|
|
179
|
+
'- Use this tool when you need to find files by name patterns\n' +
|
|
180
|
+
'- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead',
|
|
181
|
+
inputSchema: z.strictObject({
|
|
182
|
+
pattern: z.string().describe('The glob pattern to match files against'),
|
|
183
|
+
path: z.string().optional().describe('The directory to search in. Defaults to the current working directory.'),
|
|
184
|
+
limit: z.number().int().positive().optional().describe(`Maximum number of results. Defaults to ${opts.defaultGlobLimit}.`),
|
|
185
|
+
}),
|
|
186
|
+
isReadOnly: () => true,
|
|
187
|
+
prompt: fileSearchPrompt,
|
|
188
|
+
}, async (input) => {
|
|
189
|
+
const searchRoot = resolvePath(input.path ?? opts.cwd, opts.cwd);
|
|
190
|
+
const matches = [];
|
|
191
|
+
const stream = glob.stream(input.pattern, {
|
|
192
|
+
cwd: searchRoot, absolute: true, nodir: true, ignore: defaultGlobExcludes, windowsPathsNoEscape: true,
|
|
147
193
|
});
|
|
148
|
-
const
|
|
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
|
-
|
|
194
|
+
for await (const match of stream) {
|
|
195
|
+
if (typeof match === 'string')
|
|
196
|
+
matches.push(match);
|
|
197
|
+
if (matches.length >= (input.limit ?? opts.defaultGlobLimit))
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
root: searchRoot,
|
|
202
|
+
filenames: matches.slice(0, input.limit ?? opts.defaultGlobLimit),
|
|
203
|
+
numFiles: matches.length,
|
|
204
|
+
};
|
|
205
|
+
});
|
|
206
|
+
// ── Grep tool (matches Claude Code GrepTool) ─────────────────────
|
|
207
|
+
const GREP_TOOL_NAME = 'Grep';
|
|
208
|
+
const Grep = (opts) => tool({
|
|
209
|
+
name: GREP_TOOL_NAME,
|
|
210
|
+
description: 'A powerful search tool built on ripgrep\n\n' +
|
|
211
|
+
'Usage:\n' +
|
|
212
|
+
'- ALWAYS use Grep for search tasks. NEVER invoke `grep` or `rg` as a Bash command.\n' +
|
|
213
|
+
'- Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+")\n' +
|
|
214
|
+
'- Filter files with glob parameter (e.g., "*.js", "**/*.tsx") or type parameter (e.g., "js", "py", "rust")\n' +
|
|
215
|
+
'- Output modes: "content" shows matching lines, "files_with_matches" shows file paths (default), "count" shows match counts\n' +
|
|
216
|
+
'- Use Agent tool for open-ended searches requiring multiple rounds\n' +
|
|
217
|
+
'- Pattern syntax: Uses ripgrep (not grep) - literal braces need escaping\n' +
|
|
218
|
+
'- Multiline matching: By default patterns match within single lines only. For cross-line patterns use `multiline: true`',
|
|
219
|
+
inputSchema: z.strictObject({
|
|
220
|
+
pattern: z.string().describe('The regular expression pattern to search for in file contents'),
|
|
221
|
+
path: z.string().optional().describe('File or directory to search in. Defaults to current working directory.'),
|
|
222
|
+
glob: z.string().optional().describe('Glob pattern to filter files (e.g. "*.js", "*.{ts,tsx}")'),
|
|
223
|
+
output_mode: z.enum(['content', 'files_with_matches', 'count']).optional().default('files_with_matches'),
|
|
224
|
+
head_limit: z.number().int().nonnegative().optional().describe(`Output limit. Defaults to ${opts.defaultGrepLimit}. Pass 0 for unlimited.`),
|
|
225
|
+
offset: z.number().int().nonnegative().optional().default(0),
|
|
226
|
+
'-i': z.boolean().optional().default(false),
|
|
227
|
+
'-n': z.boolean().optional().default(true),
|
|
228
|
+
'-A': z.number().int().nonnegative().optional(),
|
|
229
|
+
'-B': z.number().int().nonnegative().optional(),
|
|
230
|
+
'-C': z.number().int().nonnegative().optional(),
|
|
231
|
+
context: z.number().int().nonnegative().optional(),
|
|
232
|
+
multiline: z.boolean().optional().default(false),
|
|
233
|
+
}),
|
|
234
|
+
isReadOnly: () => true,
|
|
235
|
+
prompt: fileSearchPrompt,
|
|
236
|
+
}, async (input) => {
|
|
237
|
+
const searchRoot = resolvePath(input.path ?? opts.cwd, opts.cwd);
|
|
238
|
+
const outputMode = input.output_mode ?? 'files_with_matches';
|
|
239
|
+
const limit = input.head_limit ?? opts.defaultGrepLimit;
|
|
240
|
+
const ignoreCase = input['-i'] ?? false;
|
|
241
|
+
const multiline = input.multiline ?? false;
|
|
242
|
+
const matchedFiles = [];
|
|
243
|
+
const contentLines = [];
|
|
244
|
+
const countEntries = [];
|
|
245
|
+
let totalMatches = 0;
|
|
246
|
+
const before = input['-C'] ?? input.context ?? input['-B'] ?? 0;
|
|
247
|
+
const after = input['-C'] ?? input.context ?? input['-A'] ?? 0;
|
|
248
|
+
function buildRegex(pattern, global = false) {
|
|
249
|
+
const f = [ignoreCase ? 'i' : '', multiline ? 's' : '', global ? 'g' : ''].filter(Boolean).join('');
|
|
250
|
+
return new RegExp(pattern, f);
|
|
251
|
+
}
|
|
252
|
+
const files = await findFiles(searchRoot, input.glob);
|
|
253
|
+
for (const f of files) {
|
|
254
|
+
try {
|
|
255
|
+
const buffer = await readFile(f);
|
|
256
|
+
if (isProbablyBinary(buffer))
|
|
204
257
|
continue;
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
const relativePath = toDisplayPath(searchRoot, absolutePath);
|
|
258
|
+
const content = buffer.toString('utf-8');
|
|
259
|
+
const relPath = path.relative(searchRoot, f) || f;
|
|
208
260
|
if (outputMode === 'files_with_matches') {
|
|
209
|
-
if (
|
|
210
|
-
|
|
211
|
-
multiline: input.multiline ?? false,
|
|
212
|
-
}).test(content)) {
|
|
213
|
-
matchedFiles.push(relativePath);
|
|
214
|
-
}
|
|
215
|
-
continue;
|
|
261
|
+
if (buildRegex(input.pattern).test(content))
|
|
262
|
+
matchedFiles.push(relPath);
|
|
216
263
|
}
|
|
217
|
-
if (outputMode === 'count') {
|
|
218
|
-
const
|
|
219
|
-
if (
|
|
220
|
-
totalMatches +=
|
|
221
|
-
countEntries.push({
|
|
264
|
+
else if (outputMode === 'count') {
|
|
265
|
+
const count = [...content.matchAll(buildRegex(input.pattern, true))].length;
|
|
266
|
+
if (count > 0) {
|
|
267
|
+
totalMatches += count;
|
|
268
|
+
countEntries.push({ file: relPath, count });
|
|
222
269
|
}
|
|
223
|
-
continue;
|
|
224
270
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
});
|
|
231
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
232
|
-
if (!matcher.test(lines[index] ?? '')) {
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
const start = Math.max(0, index - beforeContext);
|
|
236
|
-
const end = Math.min(lines.length - 1, index + afterContext);
|
|
237
|
-
for (let lineIndex = start; lineIndex <= end; lineIndex += 1) {
|
|
238
|
-
if (visited.has(lineIndex)) {
|
|
271
|
+
else {
|
|
272
|
+
const lines = content.split(/\r?\n/);
|
|
273
|
+
const visited = new Set();
|
|
274
|
+
for (let i = 0; i < lines.length; i++) {
|
|
275
|
+
if (!buildRegex(input.pattern).test(lines[i]))
|
|
239
276
|
continue;
|
|
277
|
+
for (let j = Math.max(0, i - before); j <= Math.min(lines.length - 1, i + after); j++) {
|
|
278
|
+
if (visited.has(j))
|
|
279
|
+
continue;
|
|
280
|
+
visited.add(j);
|
|
281
|
+
contentLines.push(input['-n'] === false ? `${relPath}:${lines[j]}` : `${relPath}:${j + 1}:${lines[j]}`);
|
|
240
282
|
}
|
|
241
|
-
visited.add(lineIndex);
|
|
242
|
-
const prefix = input['-n'] === false ? `${relativePath}:` : `${relativePath}:${lineIndex + 1}:`;
|
|
243
|
-
contentLines.push(`${prefix}${lines[lineIndex] ?? ''}`);
|
|
244
283
|
}
|
|
245
284
|
}
|
|
246
285
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
totalMatches: matchedFiles.length,
|
|
254
|
-
appliedLimit: paged.appliedLimit,
|
|
255
|
-
appliedOffset: offset,
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
if (outputMode === 'count') {
|
|
259
|
-
const countStrings = countEntries.map((entry) => `${entry.filePath}:${entry.count}`);
|
|
260
|
-
const paged = applyLimit(countStrings, limit, offset);
|
|
261
|
-
return {
|
|
262
|
-
mode: outputMode,
|
|
263
|
-
root: searchRoot,
|
|
264
|
-
filenames: paged.items,
|
|
265
|
-
totalMatches,
|
|
266
|
-
appliedLimit: paged.appliedLimit,
|
|
267
|
-
appliedOffset: offset,
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
const paged = applyLimit(contentLines, limit, offset);
|
|
271
|
-
return {
|
|
272
|
-
mode: outputMode,
|
|
273
|
-
root: searchRoot,
|
|
274
|
-
filenames: paged.items,
|
|
275
|
-
totalMatches: contentLines.length,
|
|
276
|
-
appliedLimit: paged.appliedLimit,
|
|
277
|
-
appliedOffset: offset,
|
|
278
|
-
};
|
|
279
|
-
});
|
|
280
|
-
return [Read, Write, Edit, Glob, Grep];
|
|
281
|
-
}
|
|
282
|
-
function requireAbsolutePath(filePath) {
|
|
283
|
-
if (!path.isAbsolute(filePath) && !filePath.startsWith('~')) {
|
|
284
|
-
throw new ToolExecutionError('filesystem', `Expected an absolute path, received "${filePath}".`);
|
|
286
|
+
catch { /* skip unreadable files */ }
|
|
287
|
+
}
|
|
288
|
+
const off = input.offset ?? 0;
|
|
289
|
+
if (outputMode === 'files_with_matches') {
|
|
290
|
+
const sliced = limit === 0 ? matchedFiles.slice(off) : matchedFiles.slice(off, off + limit);
|
|
291
|
+
return { mode: outputMode, root: searchRoot, filenames: sliced, totalMatches: matchedFiles.length };
|
|
285
292
|
}
|
|
286
|
-
|
|
293
|
+
if (outputMode === 'count') {
|
|
294
|
+
const entries = countEntries.map(e => `${e.file}:${e.count}`);
|
|
295
|
+
const sliced = limit === 0 ? entries.slice(off) : entries.slice(off, off + limit);
|
|
296
|
+
return { mode: outputMode, root: searchRoot, filenames: sliced, totalMatches };
|
|
297
|
+
}
|
|
298
|
+
const sliced = limit === 0 ? contentLines.slice(off) : contentLines.slice(off, off + limit);
|
|
299
|
+
return { mode: outputMode, root: searchRoot, filenames: sliced, totalMatches: contentLines.length };
|
|
300
|
+
});
|
|
301
|
+
// ── Factory ─────────────────────────────────────────────────────
|
|
302
|
+
export function createActoviqFileTools(options = {}) {
|
|
303
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
304
|
+
const maxReadLines = options.maxReadLines ?? DEFAULT_MAX_READ_LINES;
|
|
305
|
+
const defaultGlobLimit = options.defaultGlobLimit ?? DEFAULT_GLOB_LIMIT;
|
|
306
|
+
const defaultGrepLimit = options.defaultGrepLimit ?? DEFAULT_GREP_LIMIT;
|
|
307
|
+
const readState = new Map();
|
|
308
|
+
return [
|
|
309
|
+
Read({ cwd, maxReadLines, readState }),
|
|
310
|
+
Write({ cwd, readState }),
|
|
311
|
+
Edit({ cwd, readState }),
|
|
312
|
+
Glob({ cwd, defaultGlobLimit }),
|
|
313
|
+
Grep({ cwd, defaultGrepLimit }),
|
|
314
|
+
];
|
|
287
315
|
}
|
|
288
|
-
|
|
316
|
+
// ── Helpers ─────────────────────────────────────────────────────
|
|
317
|
+
function resolvePath(filePath, cwd) {
|
|
289
318
|
if (filePath.startsWith('~')) {
|
|
290
319
|
return path.resolve(os.homedir(), filePath.slice(1));
|
|
291
320
|
}
|
|
321
|
+
if (!path.isAbsolute(filePath)) {
|
|
322
|
+
throw new ToolExecutionError('filesystem', `Expected an absolute path, received "${filePath}".`);
|
|
323
|
+
}
|
|
292
324
|
return path.resolve(filePath);
|
|
293
325
|
}
|
|
294
|
-
function
|
|
295
|
-
readState.set(filePath, { mtimeMs });
|
|
296
|
-
}
|
|
297
|
-
function ensurePreviouslyRead(readState, filePath, currentMtimeMs, toolName) {
|
|
326
|
+
function ensurePreviouslyRead(readState, filePath, mtimeMs, toolName) {
|
|
298
327
|
const remembered = readState.get(filePath);
|
|
299
328
|
if (!remembered) {
|
|
300
329
|
throw new ToolExecutionError(toolName, `${toolName} requires the file to be read first: ${filePath}`);
|
|
301
330
|
}
|
|
302
|
-
if (Math.floor(
|
|
331
|
+
if (Math.floor(mtimeMs) > Math.floor(remembered.mtimeMs)) {
|
|
303
332
|
throw new ToolExecutionError(toolName, `The file changed after it was last read. Read it again before using ${toolName}: ${filePath}`);
|
|
304
333
|
}
|
|
305
334
|
}
|
|
306
|
-
async function
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
return buffer.toString('utf8');
|
|
335
|
+
async function findFiles(searchRoot, globPattern) {
|
|
336
|
+
if (!globPattern)
|
|
337
|
+
return listAllFiles(searchRoot);
|
|
338
|
+
return listGlobMatches(globPattern, searchRoot);
|
|
312
339
|
}
|
|
313
|
-
function
|
|
314
|
-
|
|
315
|
-
|
|
340
|
+
async function listGlobMatches(pattern, root) {
|
|
341
|
+
const matches = [];
|
|
342
|
+
for await (const m of glob.stream(pattern, { cwd: root, absolute: true, nodir: true, ignore: defaultGlobExcludes, windowsPathsNoEscape: true })) {
|
|
343
|
+
if (typeof m === 'string')
|
|
344
|
+
matches.push(m);
|
|
316
345
|
}
|
|
317
|
-
return
|
|
318
|
-
.map((line, index) => `${String(startLineNumber + index).padStart(6, ' ')}\t${line}`)
|
|
319
|
-
.join('\n');
|
|
346
|
+
return matches;
|
|
320
347
|
}
|
|
321
|
-
async function
|
|
348
|
+
async function listAllFiles(dir) {
|
|
349
|
+
const results = [];
|
|
322
350
|
try {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
function replaceFirst(content, oldValue, newValue) {
|
|
336
|
-
const index = content.indexOf(oldValue);
|
|
337
|
-
if (index < 0) {
|
|
338
|
-
return content;
|
|
339
|
-
}
|
|
340
|
-
return `${content.slice(0, index)}${newValue}${content.slice(index + oldValue.length)}`;
|
|
341
|
-
}
|
|
342
|
-
function resolveSearchRoot(inputPath, context, fallbackCwd) {
|
|
343
|
-
if (!inputPath) {
|
|
344
|
-
return context.cwd || fallbackCwd;
|
|
345
|
-
}
|
|
346
|
-
if (path.isAbsolute(inputPath)) {
|
|
347
|
-
return path.resolve(inputPath);
|
|
348
|
-
}
|
|
349
|
-
return path.resolve(context.cwd || fallbackCwd, inputPath);
|
|
350
|
-
}
|
|
351
|
-
const defaultGlobExcludes = [
|
|
352
|
-
'**/.git/**',
|
|
353
|
-
'**/node_modules/**',
|
|
354
|
-
'**/.svn/**',
|
|
355
|
-
'**/.hg/**',
|
|
356
|
-
'**/.jj/**',
|
|
357
|
-
'**/.sl/**',
|
|
358
|
-
];
|
|
359
|
-
async function* iterateSearchFiles(searchRoot, globPattern) {
|
|
360
|
-
const pattern = globPattern || '**/*';
|
|
361
|
-
const matches = await listGlobMatches(pattern, searchRoot);
|
|
362
|
-
for (const match of matches) {
|
|
363
|
-
yield match;
|
|
351
|
+
for (const entry of await readdirSafe(dir)) {
|
|
352
|
+
const full = path.join(dir, entry.name);
|
|
353
|
+
if (entry.isDirectory()) {
|
|
354
|
+
if (entry.name === '.git' || entry.name === 'node_modules')
|
|
355
|
+
continue;
|
|
356
|
+
results.push(...await listAllFiles(full));
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
results.push(full);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
364
362
|
}
|
|
363
|
+
catch { /* skip */ }
|
|
364
|
+
return results;
|
|
365
365
|
}
|
|
366
|
-
async function
|
|
367
|
-
const
|
|
368
|
-
cwd: searchRoot,
|
|
369
|
-
absolute: true,
|
|
370
|
-
nodir: true,
|
|
371
|
-
ignore: defaultGlobExcludes,
|
|
372
|
-
windowsPathsNoEscape: true,
|
|
373
|
-
});
|
|
374
|
-
return typeof limit === 'number' ? matches.slice(0, limit) : matches;
|
|
375
|
-
}
|
|
376
|
-
function buildSearchRegex(source, options = {}) {
|
|
377
|
-
const flags = new Set();
|
|
378
|
-
if (options.ignoreCase) {
|
|
379
|
-
flags.add('i');
|
|
380
|
-
}
|
|
381
|
-
if (options.multiline) {
|
|
382
|
-
flags.add('s');
|
|
383
|
-
}
|
|
384
|
-
if (options.global) {
|
|
385
|
-
flags.add('g');
|
|
386
|
-
}
|
|
366
|
+
async function readdirSafe(dir) {
|
|
367
|
+
const fs = await import('node:fs/promises');
|
|
387
368
|
try {
|
|
388
|
-
return
|
|
389
|
-
}
|
|
390
|
-
catch (error) {
|
|
391
|
-
throw new ToolExecutionError('Grep', `Invalid regular expression "${source}": ${String(error)}`);
|
|
369
|
+
return (await fs.readdir(dir, { withFileTypes: true }));
|
|
392
370
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
if (limit === 0) {
|
|
396
|
-
return { items: items.slice(offset), appliedLimit: undefined };
|
|
397
|
-
}
|
|
398
|
-
const effectiveLimit = limit ?? DEFAULT_GREP_LIMIT;
|
|
399
|
-
return {
|
|
400
|
-
items: items.slice(offset, offset + effectiveLimit),
|
|
401
|
-
appliedLimit: items.length - offset > effectiveLimit ? effectiveLimit : undefined,
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
function serializeGrepOutput(output) {
|
|
405
|
-
if (output.filenames.length === 0) {
|
|
406
|
-
return 'No matches found';
|
|
407
|
-
}
|
|
408
|
-
return output.filenames.join('\n');
|
|
409
|
-
}
|
|
410
|
-
function toDisplayPath(searchRoot, absolutePath) {
|
|
411
|
-
const relativePath = path.relative(searchRoot, absolutePath);
|
|
412
|
-
if (!relativePath || relativePath.startsWith('..')) {
|
|
413
|
-
return absolutePath;
|
|
371
|
+
catch {
|
|
372
|
+
return [];
|
|
414
373
|
}
|
|
415
|
-
return relativePath;
|
|
416
374
|
}
|
|
417
375
|
function isProbablyBinary(buffer) {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
if (buffer[index] === BINARY_ZERO_BYTE) {
|
|
376
|
+
for (let i = 0; i < Math.min(buffer.length, 1024); i++) {
|
|
377
|
+
if (buffer[i] === BINARY_ZERO_BYTE)
|
|
421
378
|
return true;
|
|
422
|
-
}
|
|
423
379
|
}
|
|
424
380
|
return false;
|
|
425
381
|
}
|
|
382
|
+
function isImagePath(filePath) {
|
|
383
|
+
return /\.(png|jpe?g|gif|webp|bmp)$/i.test(filePath);
|
|
384
|
+
}
|
|
385
|
+
function isPDFPath(filePath) {
|
|
386
|
+
return filePath.toLowerCase().endsWith('.pdf');
|
|
387
|
+
}
|
|
388
|
+
function escapeRegex(str) {
|
|
389
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
390
|
+
}
|
|
426
391
|
//# sourceMappingURL=actoviqFileTools.js.map
|