u
This commit is contained in:
@@ -1,12 +1,11 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { defineTool, ToolError, DirectReturnError } from 'libllmorbiter';
|
||||
import { defineTool, ToolError } from 'libllmorbiter';
|
||||
import { isReadOnlyDatabasePath } from './vfs.js';
|
||||
import { isSkillPath, renderSkill, parseSkill, SKILL_EXT, skillFileName } from './skill.js';
|
||||
import { prepareVfs, syncVfsToDb } from './vfs.js';
|
||||
import { TOOL_DESCRIPTIONS, PATCH_PARAMETER_DESCRIPTIONS } from './prompts.js';
|
||||
import { getStore } from '../db/store.js';
|
||||
import { parseDraftPath, requestDraftRead, notifyDraftUpdate, getCachedDraft } from '../services/draftManager.js';
|
||||
|
||||
/**
|
||||
* patch — applies edit operations to files in the virtual filesystem (VFS).
|
||||
@@ -64,9 +63,9 @@ function toLines(text) {
|
||||
|
||||
/**
|
||||
* Creates the patch tool bound to a company's virtual filesystem.
|
||||
* @param {{ googleAuthId: number, conversationId?: number }} ctx
|
||||
* @param {{ googleAuthId: number }} ctx
|
||||
*/
|
||||
export function createPatchTool({ googleAuthId, conversationId }) {
|
||||
export function createPatchTool({ googleAuthId }) {
|
||||
return defineTool({
|
||||
name: 'patch',
|
||||
description: TOOL_DESCRIPTIONS.patch,
|
||||
@@ -177,12 +176,7 @@ export function createPatchTool({ googleAuthId, conversationId }) {
|
||||
);
|
||||
}
|
||||
|
||||
const draftInfo = parseDraftPath(clean);
|
||||
if (draftInfo.isFolder) {
|
||||
throw new ToolError(`Operation at index ${i}: "${clean}" is a directory and cannot be patched.`);
|
||||
}
|
||||
|
||||
validated.push({ index: i, op, clean, abs, isSkill: looksSkillByExt, skillBody, draftInfo });
|
||||
validated.push({ index: i, op, clean, abs, isSkill: looksSkillByExt, skillBody });
|
||||
}
|
||||
|
||||
// ── Apply ──────────────────────────────────────────────────────
|
||||
@@ -190,57 +184,7 @@ export function createPatchTool({ googleAuthId, conversationId }) {
|
||||
const touchedSkills = new Set();
|
||||
|
||||
for (const v of validated) {
|
||||
const { op, clean, abs, isSkill, draftInfo } = v;
|
||||
|
||||
if (draftInfo?.isDraft) {
|
||||
const isCreate = op.search === null || op.search === undefined || op.search === '';
|
||||
let original = '';
|
||||
if (!isCreate) {
|
||||
try {
|
||||
const readVal = await requestDraftRead({ userId: googleAuthId, conversationId, name: draftInfo.name });
|
||||
original = typeof readVal === 'string' ? readVal : JSON.stringify(readVal ?? {}, null, 2);
|
||||
} catch (err) {
|
||||
if (err instanceof DirectReturnError) throw err;
|
||||
const cached = getCachedDraft({ userId: googleAuthId, name: draftInfo.name });
|
||||
if (cached) {
|
||||
original = typeof cached.content === 'string' ? cached.content : JSON.stringify(cached.content ?? {}, null, 2);
|
||||
} else {
|
||||
throw new ToolError(`Cannot read draft "${clean}" before patching: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let draftUpdated;
|
||||
if (isCreate) {
|
||||
draftUpdated = typeof op.replace === 'string' ? op.replace : JSON.stringify(op.replace, null, 2);
|
||||
} else {
|
||||
const haystack = original.includes('\r\n') ? original.replace(/\r\n/g, '\n') : original;
|
||||
const needle = typeof op.search === 'string' ? op.search.replace(/\r\n/g, '\n') : '';
|
||||
if (!needle) {
|
||||
throw new ToolError(`Operation at index ${v.index} ("${clean}") has an empty "search". Use "search": null to overwrite.`);
|
||||
}
|
||||
const at = haystack.indexOf(needle);
|
||||
if (at === -1) {
|
||||
throw new ToolError(`SEARCH/REPLACE failed in "${clean}": the search text was not found. Read the draft again with read_file.`);
|
||||
}
|
||||
const replacement = typeof op.replace === 'string' ? op.replace : JSON.stringify(op.replace, null, 2);
|
||||
draftUpdated = haystack.slice(0, at) + replacement + haystack.slice(at + needle.length);
|
||||
}
|
||||
|
||||
let parsed = draftUpdated;
|
||||
try { parsed = JSON.parse(draftUpdated); } catch {}
|
||||
const store = await getStore();
|
||||
const row = await store.google_auth.getById(googleAuthId);
|
||||
const companyId = row?.selectedCompanyId;
|
||||
notifyDraftUpdate({
|
||||
companyId,
|
||||
userId: googleAuthId,
|
||||
name: draftInfo.name,
|
||||
content: parsed,
|
||||
});
|
||||
applied.push(`PATCH ${clean}`);
|
||||
continue;
|
||||
}
|
||||
const { op, clean, abs, isSkill } = v;
|
||||
|
||||
const isCreate = op.search === null || op.search === undefined || op.search === '';
|
||||
const exists = fs.existsSync(abs);
|
||||
@@ -318,10 +262,7 @@ export function createPatchTool({ googleAuthId, conversationId }) {
|
||||
if (isSkill) touchedSkills.add(clean);
|
||||
}
|
||||
|
||||
// Persist immediately for disk files — unlike javascript_execute, there is no trailing sync, so a
|
||||
// text edit would otherwise live only in the on-disk cache until some other tool ran.
|
||||
const hasDiskWrites = validated.some((v) => !v.draftInfo?.isDraft);
|
||||
if (hasDiskWrites) {
|
||||
if (applied.length > 0) {
|
||||
try {
|
||||
await syncVfsToDb({ googleAuthId, vfsRoot });
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user