This commit is contained in:
seb
2026-10-16 16:24:58 +02:00
parent f506dd35be
commit 3b875c9f9a
14 changed files with 28 additions and 1228 deletions

View File

@@ -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) {