"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerTextTools = registerTextTools;
const sdk_1 = require("@lmstudio/sdk");
const zod_1 = require("zod");
const fileUtilities_1 = require("../fileUtilities");
const securityEnhanced_1 = require("../securityEnhanced");
const logger_1 = require("../logger");
// ── Regex Utilities ──
function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
/**
* Count all matches in a line using an existing global regex.
* O(n) where n = number of matches. Resets lastIndex for each call.
*/
function countMatchesInLine(line, regex) {
let count = 0;
regex.lastIndex = 0; // Reset for this independent line
let match;
while ((match = regex.exec(line)) !== null) {
count++;
if (match[0].length === 0)
regex.lastIndex++; // Avoid infinite loop on zero-length matches
}
return count;
}
/**
* Find and replace up to `max` occurrences in a line.
* O(k * m) where k = max replacements, m = position of k-th match.
* Stops immediately after reaching max (more efficient than .replace() which scans entire string).
*/
function findAndReplace(line, regex, replacement, max) {
let count = 0;
const parts = [];
let lastIndex = 0;
while (count < max) {
regex.lastIndex = lastIndex; // Reset to continue from where we left off
const match = regex.exec(line);
if (!match || match.index === undefined)
break;
parts.push(line.slice(lastIndex, match.index));
parts.push(replacement);
count++;
lastIndex = match.index + match[0].length;
if (match[0].length === 0)
lastIndex++; // Avoid infinite loop on zero-length matches
}
parts.push(line.slice(lastIndex));
return { replaced: parts.join(""), count };
}
/**
* Process a range of lines for replacement, applying pattern up to maxReplacements.
*/
function processReplacementLines(lines, fromLine, toLine, pattern, replacement, maxReplacements) {
let replacementsMade = 0;
const result = [...lines]; // Shallow copy — we only mutate specific indices
for (let i = fromLine; i < Math.min(toLine, lines.length); i++) {
if (maxReplacements !== undefined && replacementsMade >= maxReplacements)
break;
const remaining = maxReplacements !== undefined ? maxReplacements - replacementsMade : Infinity;
const { count } = findAndReplace(result[i], pattern, replacement, remaining);
if (count > 0) {
result[i] = findAndReplace(result[i], pattern, replacement, remaining).replaced;
replacementsMade += count;
}
}
return { count: replacementsMade, result };
}
/**
* Build a context snippet for display after changes.
*/
function buildChangeSnippet(lines, fromLine, toLine) {
const snippets = [];
const start = Math.max(0, fromLine - 2);
const end = Math.min(lines.length, toLine + 3);
for (let i = start; i < end; i++) {
const lineNum = i + 1;
const prefix = lineNum < 10 ? " " : "";
snippets.push(`${prefix}${lineNum}: ${lines[i]}`);
}
return snippets.join("\n");
}
// ── Tool Registration ──
async function registerTextTools() {
const tools = [];
// ── replace_text ──
tools.push((0, sdk_1.tool)({
name: "replace_text",
description: "Find and replace text within a file. Supports literal strings and regex patterns.",
parameters: {
file_path: zod_1.z.string().describe("Path to the file to modify"),
find: zod_1.z.string().describe("Text or regex pattern to find"),
replace_with: zod_1.z.string().describe("Text to replace matches with"),
regex: zod_1.z.boolean().optional().describe("Treat 'find' as a regex pattern (default: false)"),
case_sensitive: zod_1.z.boolean().optional().describe("Case sensitivity (default: true for regex, true for literal)"),
max_replacements: zod_1.z.number().optional().describe("Maximum number of replacements to make (default: all)"),
start_line: zod_1.z.number().optional().describe("Line number to start replacements from (1-based, default: 1)"),
end_line: zod_1.z.number().optional().describe("Line number to stop replacements at (1-based, default: end of file)"),
create_backup: zod_1.z.boolean().optional().describe("Create a .bak backup before modifying (default: false)"),
},
implementation: async ({ file_path, find, replace_with, regex, case_sensitive, max_replacements, start_line, end_line, create_backup }) => {
try {
const validation = (0, securityEnhanced_1.validateWritePath)(file_path);
if (!validation.valid)
return { success: false, replacements_made: 0, error: validation.error };
const safePath = validation.sanitized;
const readResult = (0, fileUtilities_1.readFile)(safePath);
if (!readResult)
return { success: false, replacements_made: 0, error: `Failed to read file: ${safePath}` };
let content = readResult;
if (create_backup)
(0, fileUtilities_1.writeFileSync)(safePath + ".bak", content);
const lines = content.split("\n");
const fromLine = (start_line ?? 1) - 1;
const effectiveToLine = end_line !== undefined ? Math.min(end_line, lines.length) : lines.length;
if (fromLine < 0 || fromLine >= lines.length) {
return { success: false, replacements_made: 0, error: `Invalid start_line: ${start_line}. File has ${lines.length} lines.` };
}
const caseFlag = (case_sensitive ?? true) ? "" : "i";
let pattern;
if (regex) {
try {
pattern = new RegExp(find, caseFlag + "g");
}
catch (err) {
return { success: false, replacements_made: 0, error: `Invalid regex pattern '${find}': ${err.message}` };
}
}
else {
const escaped = escapeRegExp(find);
pattern = new RegExp(escaped, caseFlag + "g");
}
// Use findAndReplace directly instead of replaceInLine (more efficient)
let replacementsMade = 0;
for (let i = fromLine; i < effectiveToLine; i++) {
if (max_replacements !== undefined && replacementsMade >= max_replacements)
break;
const remaining = max_replacements !== undefined ? Math.max(0, max_replacements - replacementsMade) : Infinity;
const { replaced, count } = findAndReplace(lines[i], pattern, replace_with, remaining);
if (count > 0) {
lines[i] = replaced;
replacementsMade += count;
}
}
(0, fileUtilities_1.writeFileSync)(safePath, lines.join("\n"));
return {
success: true,
replacements_made: replacementsMade,
file_path: safePath,
file_size: content.length,
changed: replacementsMade > 0,
snippet: replacementsMade > 0 ? buildChangeSnippet(lines, fromLine, effectiveToLine) : undefined,
};
}
catch (err) {
logger_1.logger.error('replace_text failed', { error: err.message });
return { success: false, replacements_made: 0, error: err.message };
}
},
}));
// ── count_text ──
tools.push((0, sdk_1.tool)({
name: "count_text",
description: "Count occurrences of text or regex patterns in a file.",
parameters: {
file_path: zod_1.z.string().describe("Path to the file to search"),
pattern: zod_1.z.string().describe("Text or regex pattern to count"),
regex: zod_1.z.boolean().optional().describe("Treat 'pattern' as a regex pattern (default: false)"),
case_sensitive: zod_1.z.boolean().optional().describe("Case sensitivity (default: true)"),
start_line: zod_1.z.number().optional().describe("Line number to start counting from (1-based, default: 1)"),
end_line: zod_1.z.number().optional().describe("Line number to stop counting at (1-based, default: end of file)"),
},
implementation: async ({ file_path, pattern, regex, case_sensitive, start_line, end_line }) => {
try {
const validation = (0, securityEnhanced_1.sanitizeFilePath)(file_path);
if (!validation.valid)
return { success: false, count: 0, error: validation.error };
const safePath = validation.sanitized;
const readResult = (0, fileUtilities_1.readFile)(safePath);
if (!readResult)
return { success: false, count: 0, error: `Failed to read file: ${safePath}` };
const lines = readResult.split("\n");
const fromLine = (start_line ?? 1) - 1;
const effectiveToLine = end_line !== undefined ? Math.min(end_line, lines.length) : lines.length;
if (fromLine < 0 || fromLine >= lines.length) {
return { success: false, count: 0, error: `Invalid start_line: ${start_line}. File has ${lines.length} lines.` };
}
const caseFlag = (case_sensitive ?? true) ? "" : "i";
let regExp;
if (regex) {
try {
regExp = new RegExp(pattern, caseFlag + "g");
}
catch (err) {
return { success: false, count: 0, error: `Invalid regex pattern '${pattern}': ${err.message}` };
}
}
else {
const escaped = escapeRegExp(pattern);
regExp = new RegExp(escaped, caseFlag + "g");
}
let totalCount = 0;
const matchLines = [];
for (let i = fromLine; i < effectiveToLine; i++) {
const count = countMatchesInLine(lines[i], regExp);
if (count > 0) {
totalCount += count;
if (matchLines.length < 20)
matchLines.push(i + 1);
}
}
return {
success: true,
count: totalCount,
pattern,
regex_used: regex ?? false,
case_sensitive: case_sensitive ?? true,
match_lines: matchLines,
match_lines_truncated: matchLines.length >= 20,
total_lines_scanned: effectiveToLine - fromLine,
};
}
catch (err) {
logger_1.logger.error('count_text failed', { error: err.message });
return { success: false, count: 0, error: err.message };
}
},
}));
// ── insert_text ──
tools.push((0, sdk_1.tool)({
name: "insert_text",
description: "Insert text at a specific line in a file.",
parameters: {
file_path: zod_1.z.string().describe("Path to the file to modify"),
insert_at_line: zod_1.z.number().describe("Line number to insert before (1-based)"),
content: zod_1.z.string().describe("Text content to insert (may contain newlines)"),
},
implementation: async ({ file_path, insert_at_line, content }) => {
try {
const validation = (0, securityEnhanced_1.validateWritePath)(file_path);
if (!validation.valid)
return { success: false, error: validation.error };
const safePath = validation.sanitized;
const readResult = (0, fileUtilities_1.readFile)(safePath);
if (!readResult)
return { success: false, error: `Failed to read file: ${safePath}` };
const lines = readResult.split("\n");
let insertIndex;
if (insert_at_line === 0) {
insertIndex = 0;
}
else if (insert_at_line > lines.length) {
insertIndex = lines.length;
}
else {
insertIndex = insert_at_line - 1;
}
if (insertIndex < 0 || insertIndex > lines.length) {
return { success: false, error: `Invalid insert_at_line: ${insert_at_line}. File has ${lines.length} lines.` };
}
const insertedLines = content.split("\n");
lines.splice(insertIndex, 0, ...insertedLines);
(0, fileUtilities_1.writeFileSync)(safePath, lines.join("\n"));
return {
success: true,
file_path: safePath,
lines_inserted: insertedLines.length,
new_total_lines: lines.length,
insertion_point: insertIndex + 1,
};
}
catch (err) {
logger_1.logger.error('insert_text failed', { error: err.message });
return { success: false, error: err.message };
}
},
}));
// ── delete_lines ──
tools.push((0, sdk_1.tool)({
name: "delete_lines",
description: "Delete a range of lines from a file.",
parameters: {
file_path: zod_1.z.string().describe("Path to the file to modify"),
start_line: zod_1.z.number().describe("First line to delete (1-based)"),
end_line: zod_1.z.number().describe("Last line to delete (1-based)"),
},
implementation: async ({ file_path, start_line, end_line }) => {
try {
const validation = (0, securityEnhanced_1.validateWritePath)(file_path);
if (!validation.valid)
return { success: false, lines_deleted: 0, error: validation.error };
const safePath = validation.sanitized;
const readResult = (0, fileUtilities_1.readFile)(safePath);
if (!readResult)
return { success: false, lines_deleted: 0, error: `Failed to read file: ${safePath}` };
const lines = readResult.split("\n");
const totalLines = lines.length;
const from = Math.max(1, start_line);
const to = Math.min(end_line, totalLines);
if (from > totalLines || to < 1 || from > to) {
return { success: false, lines_deleted: 0, error: `Invalid line range: ${start_line}-${end_line}. File has ${totalLines} lines.` };
}
const deletedCount = to - (from - 1);
lines.splice(from - 1, deletedCount);
(0, fileUtilities_1.writeFileSync)(safePath, lines.join("\n"));
// Build context snippet around deletion point
const contextStart = Math.max(0, from - 4);
const contextEnd = Math.min(lines.length, from + 3);
const contextLines = [];
for (let i = contextStart; i < contextEnd; i++) {
const lineNum = i + 1;
const prefix = lineNum < 10 ? " " : "";
contextLines.push(`${prefix}${lineNum}: ${lines[i]}`);
}
return {
success: true,
lines_deleted: deletedCount,
file_path: safePath,
new_total_lines: lines.length,
context: contextLines.join("\n"),
};
}
catch (err) {
logger_1.logger.error('delete_lines failed', { error: err.message });
return { success: false, lines_deleted: 0, error: err.message };
}
},
}));
return tools;
}
//# sourceMappingURL=text_tools.js.map