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.
Files changed (168) hide show
  1. package/README-zh.md +19 -3
  2. package/README.md +19 -3
  3. package/bin/actoviq-interactive-agent.js +5 -0
  4. package/bin/actoviq-link-runtime.js +74 -0
  5. package/bin/actoviq-react.js +5 -0
  6. package/dist/src/cli/actoviq-react.d.ts +3 -0
  7. package/dist/src/cli/actoviq-react.d.ts.map +1 -0
  8. package/dist/src/cli/actoviq-react.js +277 -0
  9. package/dist/src/cli/actoviq-react.js.map +1 -0
  10. package/dist/src/cli/bridge-interactive-agent.d.ts +2 -0
  11. package/dist/src/cli/bridge-interactive-agent.d.ts.map +1 -0
  12. package/dist/src/cli/bridge-interactive-agent.js +147 -0
  13. package/dist/src/cli/bridge-interactive-agent.js.map +1 -0
  14. package/dist/src/config/resolveRuntimeConfig.d.ts.map +1 -1
  15. package/dist/src/config/resolveRuntimeConfig.js +13 -16
  16. package/dist/src/config/resolveRuntimeConfig.js.map +1 -1
  17. package/dist/src/errors.d.ts +9 -0
  18. package/dist/src/errors.d.ts.map +1 -1
  19. package/dist/src/errors.js +9 -0
  20. package/dist/src/errors.js.map +1 -1
  21. package/dist/src/index.d.ts +20 -1
  22. package/dist/src/index.d.ts.map +1 -1
  23. package/dist/src/index.js +18 -1
  24. package/dist/src/index.js.map +1 -1
  25. package/dist/src/parity/actoviqBridgeEvents.d.ts +7 -0
  26. package/dist/src/parity/actoviqBridgeEvents.d.ts.map +1 -0
  27. package/dist/src/parity/actoviqBridgeEvents.js +133 -0
  28. package/dist/src/parity/actoviqBridgeEvents.js.map +1 -0
  29. package/dist/src/parity/actoviqBridgeSdk.d.ts +179 -0
  30. package/dist/src/parity/actoviqBridgeSdk.d.ts.map +1 -0
  31. package/dist/src/parity/actoviqBridgeSdk.js +1267 -0
  32. package/dist/src/parity/actoviqBridgeSdk.js.map +1 -0
  33. package/dist/src/parity/actoviqTranscripts.d.ts +44 -0
  34. package/dist/src/parity/actoviqTranscripts.d.ts.map +1 -0
  35. package/dist/src/parity/actoviqTranscripts.js +282 -0
  36. package/dist/src/parity/actoviqTranscripts.js.map +1 -0
  37. package/dist/src/parity/portableSessions.d.ts +35 -0
  38. package/dist/src/parity/portableSessions.d.ts.map +1 -0
  39. package/dist/src/parity/portableSessions.js +233 -0
  40. package/dist/src/parity/portableSessions.js.map +1 -0
  41. package/dist/src/prompts/systemPrompt.d.ts +15 -0
  42. package/dist/src/prompts/systemPrompt.d.ts.map +1 -0
  43. package/dist/src/prompts/systemPrompt.js +73 -0
  44. package/dist/src/prompts/systemPrompt.js.map +1 -0
  45. package/dist/src/provider/client.d.ts +1 -0
  46. package/dist/src/provider/client.d.ts.map +1 -1
  47. package/dist/src/provider/client.js +6 -5
  48. package/dist/src/provider/client.js.map +1 -1
  49. package/dist/src/provider/json-parse.d.ts +7 -0
  50. package/dist/src/provider/json-parse.d.ts.map +1 -0
  51. package/dist/src/provider/json-parse.js +69 -0
  52. package/dist/src/provider/json-parse.js.map +1 -0
  53. package/dist/src/provider/openai-client.d.ts.map +1 -1
  54. package/dist/src/provider/openai-client.js +2 -1
  55. package/dist/src/provider/openai-client.js.map +1 -1
  56. package/dist/src/provider/openai-model-api.d.ts.map +1 -1
  57. package/dist/src/provider/openai-model-api.js +2 -7
  58. package/dist/src/provider/openai-model-api.js.map +1 -1
  59. package/dist/src/runtime/actoviqApiMicrocompact.d.ts.map +1 -1
  60. package/dist/src/runtime/actoviqApiMicrocompact.js +10 -17
  61. package/dist/src/runtime/actoviqApiMicrocompact.js.map +1 -1
  62. package/dist/src/runtime/actoviqCompact.d.ts.map +1 -1
  63. package/dist/src/runtime/actoviqCompact.js +53 -1
  64. package/dist/src/runtime/actoviqCompact.js.map +1 -1
  65. package/dist/src/runtime/actoviqPermissions.d.ts.map +1 -1
  66. package/dist/src/runtime/actoviqPermissions.js +120 -54
  67. package/dist/src/runtime/actoviqPermissions.js.map +1 -1
  68. package/dist/src/runtime/agentClient.d.ts.map +1 -1
  69. package/dist/src/runtime/agentClient.js +30 -2
  70. package/dist/src/runtime/agentClient.js.map +1 -1
  71. package/dist/src/runtime/conversationEngine.d.ts.map +1 -1
  72. package/dist/src/runtime/conversationEngine.js +95 -3
  73. package/dist/src/runtime/conversationEngine.js.map +1 -1
  74. package/dist/src/runtime/denialTracking.d.ts +10 -0
  75. package/dist/src/runtime/denialTracking.d.ts.map +1 -0
  76. package/dist/src/runtime/denialTracking.js +27 -0
  77. package/dist/src/runtime/denialTracking.js.map +1 -0
  78. package/dist/src/runtime/safetyChecks.d.ts +12 -0
  79. package/dist/src/runtime/safetyChecks.d.ts.map +1 -0
  80. package/dist/src/runtime/safetyChecks.js +62 -0
  81. package/dist/src/runtime/safetyChecks.js.map +1 -0
  82. package/dist/src/runtime/tools.d.ts.map +1 -1
  83. package/dist/src/runtime/tools.js +61 -3
  84. package/dist/src/runtime/tools.js.map +1 -1
  85. package/dist/src/scheduling/cron.d.ts +14 -0
  86. package/dist/src/scheduling/cron.d.ts.map +1 -0
  87. package/dist/src/scheduling/cron.js +157 -0
  88. package/dist/src/scheduling/cron.js.map +1 -0
  89. package/dist/src/scheduling/index.d.ts +3 -0
  90. package/dist/src/scheduling/index.d.ts.map +1 -0
  91. package/dist/src/scheduling/index.js +3 -0
  92. package/dist/src/scheduling/index.js.map +1 -0
  93. package/dist/src/scheduling/scheduler.d.ts +32 -0
  94. package/dist/src/scheduling/scheduler.d.ts.map +1 -0
  95. package/dist/src/scheduling/scheduler.js +196 -0
  96. package/dist/src/scheduling/scheduler.js.map +1 -0
  97. package/dist/src/tools/actoviqCoreTools.d.ts +23 -0
  98. package/dist/src/tools/actoviqCoreTools.d.ts.map +1 -0
  99. package/dist/src/tools/actoviqCoreTools.js +32 -0
  100. package/dist/src/tools/actoviqCoreTools.js.map +1 -0
  101. package/dist/src/tools/actoviqFileTools.d.ts.map +1 -1
  102. package/dist/src/tools/actoviqFileTools.js +338 -373
  103. package/dist/src/tools/actoviqFileTools.js.map +1 -1
  104. package/dist/src/tools/actoviqMiscTools.d.ts +11 -0
  105. package/dist/src/tools/actoviqMiscTools.d.ts.map +1 -0
  106. package/dist/src/tools/actoviqMiscTools.js +89 -0
  107. package/dist/src/tools/actoviqMiscTools.js.map +1 -0
  108. package/dist/src/tools/actoviqNotebookEdit.d.ts +4 -0
  109. package/dist/src/tools/actoviqNotebookEdit.d.ts.map +1 -0
  110. package/dist/src/tools/actoviqNotebookEdit.js +23 -0
  111. package/dist/src/tools/actoviqNotebookEdit.js.map +1 -0
  112. package/dist/src/tools/actoviqShellTools.d.ts +5 -0
  113. package/dist/src/tools/actoviqShellTools.d.ts.map +1 -0
  114. package/dist/src/tools/actoviqShellTools.js +35 -0
  115. package/dist/src/tools/actoviqShellTools.js.map +1 -0
  116. package/dist/src/tools/actoviqTaskTools.d.ts +12 -0
  117. package/dist/src/tools/actoviqTaskTools.d.ts.map +1 -0
  118. package/dist/src/tools/actoviqTaskTools.js +108 -0
  119. package/dist/src/tools/actoviqTaskTools.js.map +1 -0
  120. package/dist/src/tools/actoviqWebTools.d.ts +15 -0
  121. package/dist/src/tools/actoviqWebTools.d.ts.map +1 -0
  122. package/dist/src/tools/actoviqWebTools.js +395 -0
  123. package/dist/src/tools/actoviqWebTools.js.map +1 -0
  124. package/dist/src/tools/askUserQuestion/AskUserQuestionTool.d.ts +6 -0
  125. package/dist/src/tools/askUserQuestion/AskUserQuestionTool.d.ts.map +1 -0
  126. package/dist/src/tools/askUserQuestion/AskUserQuestionTool.js +43 -0
  127. package/dist/src/tools/askUserQuestion/AskUserQuestionTool.js.map +1 -0
  128. package/dist/src/tools/bash/BashTool.d.ts +14 -0
  129. package/dist/src/tools/bash/BashTool.d.ts.map +1 -0
  130. package/dist/src/tools/bash/BashTool.js +60 -0
  131. package/dist/src/tools/bash/BashTool.js.map +1 -0
  132. package/dist/src/tools/bash/prompt.d.ts +3 -0
  133. package/dist/src/tools/bash/prompt.d.ts.map +1 -0
  134. package/dist/src/tools/bash/prompt.js +63 -0
  135. package/dist/src/tools/bash/prompt.js.map +1 -0
  136. package/dist/src/tools/prompts/fileEditPrompt.d.ts +3 -0
  137. package/dist/src/tools/prompts/fileEditPrompt.d.ts.map +1 -0
  138. package/dist/src/tools/prompts/fileEditPrompt.js +14 -0
  139. package/dist/src/tools/prompts/fileEditPrompt.js.map +1 -0
  140. package/dist/src/tools/prompts/fileReadPrompt.d.ts +3 -0
  141. package/dist/src/tools/prompts/fileReadPrompt.d.ts.map +1 -0
  142. package/dist/src/tools/prompts/fileReadPrompt.js +18 -0
  143. package/dist/src/tools/prompts/fileReadPrompt.js.map +1 -0
  144. package/dist/src/tools/prompts/fileSearchPrompt.d.ts +2 -0
  145. package/dist/src/tools/prompts/fileSearchPrompt.d.ts.map +1 -0
  146. package/dist/src/tools/prompts/fileSearchPrompt.js +21 -0
  147. package/dist/src/tools/prompts/fileSearchPrompt.js.map +1 -0
  148. package/dist/src/tools/prompts/fileWritePrompt.d.ts +3 -0
  149. package/dist/src/tools/prompts/fileWritePrompt.d.ts.map +1 -0
  150. package/dist/src/tools/prompts/fileWritePrompt.js +13 -0
  151. package/dist/src/tools/prompts/fileWritePrompt.js.map +1 -0
  152. package/dist/src/tools/prompts/webFetchPrompt.d.ts +3 -0
  153. package/dist/src/tools/prompts/webFetchPrompt.d.ts.map +1 -0
  154. package/dist/src/tools/prompts/webFetchPrompt.js +13 -0
  155. package/dist/src/tools/prompts/webFetchPrompt.js.map +1 -0
  156. package/dist/src/tools/prompts/webSearchPrompt.d.ts +3 -0
  157. package/dist/src/tools/prompts/webSearchPrompt.d.ts.map +1 -0
  158. package/dist/src/tools/prompts/webSearchPrompt.js +17 -0
  159. package/dist/src/tools/prompts/webSearchPrompt.js.map +1 -0
  160. package/dist/src/tools/todo/TodoWriteTool.d.ts +6 -0
  161. package/dist/src/tools/todo/TodoWriteTool.d.ts.map +1 -0
  162. package/dist/src/tools/todo/TodoWriteTool.js +62 -0
  163. package/dist/src/tools/todo/TodoWriteTool.js.map +1 -0
  164. package/dist/src/types.d.ts +309 -2
  165. package/dist/src/types.d.ts.map +1 -1
  166. package/dist/src/types.js.map +1 -1
  167. package/package.json +35 -11
  168. package/vendor/actoviq-runtime/cli.js +54 -0
@@ -1,426 +1,391 @@
1
- import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
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
- export function createActoviqFileTools(options = {}) {
13
- const readState = new Map();
14
- const baseCwd = path.resolve(options.cwd ?? process.cwd());
15
- const maxReadLines = options.maxReadLines ?? DEFAULT_MAX_READ_LINES;
16
- const defaultGlobLimit = options.defaultGlobLimit ?? DEFAULT_GLOB_LIMIT;
17
- const defaultGrepLimit = options.defaultGrepLimit ?? DEFAULT_GREP_LIMIT;
18
- const Read = tool({
19
- name: 'Read',
20
- description: 'Read a file from the local filesystem. Returns numbered lines and remembers the file state for later edits.',
21
- inputSchema: z.object({
22
- file_path: z.string().describe('Absolute path to the file to read.'),
23
- offset: z
24
- .number()
25
- .int()
26
- .positive()
27
- .optional()
28
- .describe('1-based starting line number. Omit to start from line 1.'),
29
- limit: z
30
- .number()
31
- .int()
32
- .positive()
33
- .optional()
34
- .describe(`Maximum number of lines to read. Defaults to ${maxReadLines}.`),
35
- }),
36
- serialize: (output) => output.content,
37
- }, async (input, context) => {
38
- const filePath = requireAbsolutePath(input.file_path);
39
- const resolvedPath = normalizeAbsolutePath(filePath);
40
- const fileStats = await stat(resolvedPath);
41
- if (!fileStats.isFile()) {
42
- throw new ToolExecutionError('Read', `Path is not a file: ${resolvedPath}`);
43
- }
44
- const text = await readTextFile(resolvedPath);
45
- rememberRead(readState, resolvedPath, fileStats.mtimeMs);
46
- const lines = text.split(/\r?\n/);
47
- const startLine = input.offset ?? 1;
48
- const startIndex = Math.max(0, startLine - 1);
49
- const lineCount = input.limit ?? maxReadLines;
50
- const selectedLines = lines.slice(startIndex, startIndex + lineCount);
51
- const content = formatWithLineNumbers(selectedLines, startIndex + 1);
52
- return {
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
- const Write = tool({
62
- name: 'Write',
63
- description: 'Write a file to the local filesystem. Existing files must be read first to avoid clobbering unseen changes.',
64
- inputSchema: z.object({
65
- file_path: z.string().describe('Absolute path to the file to write.'),
66
- content: z.string().describe('The full file contents to write.'),
67
- }),
68
- serialize: (output) => `${output.type === 'create' ? 'Created' : 'Updated'} ${output.filePath}`,
69
- }, async (input) => {
70
- const resolvedPath = normalizeAbsolutePath(requireAbsolutePath(input.file_path));
71
- const existing = await safeStat(resolvedPath);
72
- if (existing?.isFile()) {
73
- ensurePreviouslyRead(readState, resolvedPath, existing.mtimeMs, 'Write');
74
- }
75
- await mkdir(path.dirname(resolvedPath), { recursive: true });
76
- await writeFile(resolvedPath, input.content, 'utf8');
77
- const finalStats = await stat(resolvedPath);
78
- rememberRead(readState, resolvedPath, finalStats.mtimeMs);
79
- return {
80
- type: existing ? 'update' : 'create',
81
- filePath: resolvedPath,
82
- bytesWritten: Buffer.byteLength(input.content, 'utf8'),
83
- };
84
- });
85
- const Edit = tool({
86
- name: 'Edit',
87
- description: 'Edit a file in place by replacing one string with another. Existing files must be read first.',
88
- inputSchema: z.object({
89
- file_path: z.string().describe('Absolute path to the file to edit.'),
90
- old_string: z.string().describe('The original text to replace.'),
91
- new_string: z.string().describe('The replacement text.'),
92
- replace_all: z.boolean().optional().default(false),
93
- }),
94
- serialize: (output) => `Edited ${output.filePath} (${output.replacements} replacement${output.replacements === 1 ? '' : 's'})`,
95
- }, async (input) => {
96
- const resolvedPath = normalizeAbsolutePath(requireAbsolutePath(input.file_path));
97
- const fileStats = await stat(resolvedPath);
98
- if (!fileStats.isFile()) {
99
- throw new ToolExecutionError('Edit', `Path is not a file: ${resolvedPath}`);
100
- }
101
- ensurePreviouslyRead(readState, resolvedPath, fileStats.mtimeMs, 'Edit');
102
- const originalContent = await readTextFile(resolvedPath);
103
- const occurrences = countOccurrences(originalContent, input.old_string);
104
- if (occurrences === 0) {
105
- throw new ToolExecutionError('Edit', `old_string was not found in ${resolvedPath}`);
106
- }
107
- if (!input.replace_all && occurrences > 1) {
108
- throw new ToolExecutionError('Edit', `old_string matched ${occurrences} locations in ${resolvedPath}. Use replace_all: true or provide a more specific old_string.`);
109
- }
110
- const updatedContent = input.replace_all
111
- ? originalContent.split(input.old_string).join(input.new_string)
112
- : replaceFirst(originalContent, input.old_string, input.new_string);
113
- await writeFile(resolvedPath, updatedContent, 'utf8');
114
- const finalStats = await stat(resolvedPath);
115
- rememberRead(readState, resolvedPath, finalStats.mtimeMs);
116
- return {
117
- filePath: resolvedPath,
118
- replacements: input.replace_all ? occurrences : 1,
119
- };
120
- });
121
- const Glob = tool({
122
- name: 'Glob',
123
- description: 'Find files by glob pattern.',
124
- inputSchema: z.object({
125
- pattern: z.string().describe('Glob pattern to match, for example **/*.ts'),
126
- path: z
127
- .string()
128
- .optional()
129
- .describe('Directory to search in. Defaults to the toolset cwd.'),
130
- limit: z
131
- .number()
132
- .int()
133
- .positive()
134
- .optional()
135
- .describe(`Maximum number of results. Defaults to ${defaultGlobLimit}.`),
136
- }),
137
- serialize: (output) => output.filenames.length ? output.filenames.join('\n') : 'No files found',
138
- }, async (input, context) => {
139
- const searchRoot = resolveSearchRoot(input.path, context, baseCwd);
140
- const limit = input.limit ?? defaultGlobLimit;
141
- const matches = await listGlobMatches(input.pattern, searchRoot, limit);
142
- return {
143
- root: searchRoot,
144
- filenames: matches,
145
- numFiles: matches.length,
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 Grep = tool({
149
- name: 'Grep',
150
- description: 'Search file contents with a regular expression. Supports content, files_with_matches, and count output modes.',
151
- inputSchema: z.object({
152
- pattern: z.string().describe('Regular expression to search for.'),
153
- path: z
154
- .string()
155
- .optional()
156
- .describe('File or directory to search in. Defaults to the toolset cwd.'),
157
- glob: z
158
- .string()
159
- .optional()
160
- .describe('Optional glob to narrow the files searched.'),
161
- output_mode: z
162
- .enum(['content', 'files_with_matches', 'count'])
163
- .optional()
164
- .default('files_with_matches'),
165
- head_limit: z
166
- .number()
167
- .int()
168
- .nonnegative()
169
- .optional()
170
- .describe(`Result limit. Defaults to ${defaultGrepLimit}. Pass 0 for unlimited.`),
171
- offset: z.number().int().nonnegative().optional().default(0),
172
- '-i': z.boolean().optional().default(false),
173
- '-n': z.boolean().optional().default(true),
174
- '-A': z.number().int().nonnegative().optional(),
175
- '-B': z.number().int().nonnegative().optional(),
176
- '-C': z.number().int().nonnegative().optional(),
177
- context: z.number().int().nonnegative().optional(),
178
- multiline: z.boolean().optional().default(false),
179
- }),
180
- serialize: (output) => serializeGrepOutput(output),
181
- }, async (input, context) => {
182
- const searchRoot = resolveSearchRoot(input.path, context, baseCwd);
183
- const outputMode = input.output_mode ?? 'files_with_matches';
184
- const limit = input.head_limit ?? defaultGrepLimit;
185
- const offset = input.offset ?? 0;
186
- const regex = buildSearchRegex(input.pattern, {
187
- ignoreCase: input['-i'] ?? false,
188
- multiline: input.multiline ?? false,
189
- global: outputMode === 'count',
190
- });
191
- const beforeContext = input['-C'] ?? input.context ?? input['-B'] ?? 0;
192
- const afterContext = input['-C'] ?? input.context ?? input['-A'] ?? 0;
193
- const matchedFiles = [];
194
- const contentLines = [];
195
- const countEntries = [];
196
- let totalMatches = 0;
197
- for await (const absolutePath of iterateSearchFiles(searchRoot, input.glob)) {
198
- const fileStats = await safeStat(absolutePath);
199
- if (!fileStats?.isFile()) {
200
- continue;
201
- }
202
- const buffer = await readFile(absolutePath);
203
- if (isProbablyBinary(buffer)) {
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 content = buffer.toString('utf8');
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 (buildSearchRegex(input.pattern, {
210
- ignoreCase: input['-i'] ?? false,
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 matches = [...content.matchAll(regex)].length;
219
- if (matches > 0) {
220
- totalMatches += matches;
221
- countEntries.push({ filePath: relativePath, count: matches });
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
- const lines = content.split(/\r?\n/);
226
- const visited = new Set();
227
- const matcher = buildSearchRegex(input.pattern, {
228
- ignoreCase: input['-i'] ?? false,
229
- multiline: false,
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
- if (outputMode === 'files_with_matches') {
248
- const paged = applyLimit(matchedFiles, limit, offset);
249
- return {
250
- mode: outputMode,
251
- root: searchRoot,
252
- filenames: paged.items,
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
- return filePath;
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
- function normalizeAbsolutePath(filePath) {
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 rememberRead(readState, filePath, mtimeMs) {
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(currentMtimeMs) > Math.floor(remembered.mtimeMs)) {
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 readTextFile(filePath) {
307
- const buffer = await readFile(filePath);
308
- if (isProbablyBinary(buffer)) {
309
- throw new ToolExecutionError('Read', `Binary files are not supported by this parity helper: ${filePath}`);
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 formatWithLineNumbers(lines, startLineNumber) {
314
- if (lines.length === 0) {
315
- return '';
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 lines
318
- .map((line, index) => `${String(startLineNumber + index).padStart(6, ' ')}\t${line}`)
319
- .join('\n');
346
+ return matches;
320
347
  }
321
- async function safeStat(filePath) {
348
+ async function listAllFiles(dir) {
349
+ const results = [];
322
350
  try {
323
- return await stat(filePath);
324
- }
325
- catch {
326
- return undefined;
327
- }
328
- }
329
- function countOccurrences(content, needle) {
330
- if (!needle) {
331
- return 0;
332
- }
333
- return content.split(needle).length - 1;
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 listGlobMatches(pattern, searchRoot, limit) {
367
- const matches = await glob(pattern, {
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 new RegExp(source, [...flags].join(''));
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
- function applyLimit(items, limit, offset = 0) {
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
- const length = Math.min(buffer.length, 1024);
419
- for (let index = 0; index < length; index += 1) {
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