This commit is contained in:
seb
2026-10-07 20:36:50 +02:00
parent 857963b1c3
commit a29ad50b74
18 changed files with 1024 additions and 220 deletions

View File

@@ -1,7 +1,6 @@
import fs from 'fs';
import path from 'path';
import { defineTool } from 'libllmorbiter';
import { getStore } from '../db/store.js';
import { isSkillPath } from './skill.js';
import { resolveVfsRoot } from './vfs.js';
@@ -40,25 +39,8 @@ export function createResetVfsTool({ googleAuthId }) {
},
},
invoke: async ({ include_skills = false } = {}) => {
const store = await getStore();
const keepSkills = include_skills !== true;
const keptSkills = [];
const existing = await store.modelvfs.list(googleAuthId);
// Phase 1 — clear the database, but only for the names we may delete. Done before
// touching disk so a store failure cannot leave the disk wiped while the database
// still claims the files exist.
for (const file of existing) {
if (keepSkills && isSkillPath(file.name)) {
keptSkills.push(file.name);
continue;
}
await store.modelvfs.delete(googleAuthId, file.name);
}
// Phase 2 — mirror the same decision onto the on-disk cache. The root comes from
// the same lookup the store used above, so the two phases cannot drift apart.
const vfsRoot = await resolveVfsRoot(googleAuthId);
let deletedCount = 0;
if (fs.existsSync(vfsRoot)) {

View File

@@ -1,7 +1,6 @@
import fs from 'fs';
import path from 'path';
import { defineTool, ToolError } from 'libllmorbiter';
import { getStore } from '../db/store.js';
import { isReadOnlyDatabasePath, prepareVfs } from './vfs.js';
import { isSkillPath } from './skill.js';
@@ -97,18 +96,12 @@ export function createRmTool({ googleAuthId }) {
const vfsRoot = await prepareVfs({ googleAuthId });
const { abs, size } = deleteVfsFile(vfsRoot, clean);
// Database first. If this throws, the file is still on disk — recoverable, and the row
// still exists to describe it. Doing it the other way round would leave a blob on
// disk that the next prepareVfs hydration would restore, silently undoing the delete.
const store = await getStore();
const res = await store.modelvfs.delete(googleAuthId, clean);
fs.unlinkSync(abs);
const wasSkill = isSkillPath(clean);
const result = {
success: true,
deleted: res?.deleted !== false,
deleted: true,
filename: clean,
size,
message: `Deleted "${clean}" (${size} bytes). This cannot be undone.`,

View File

@@ -126,7 +126,7 @@ async function vfsRootFor(store, googleAuthId) {
}
/**
* Loads default SKILL files into the selected company's VFS (both modelvfs table and disk cache).
* Loads default SKILL files into the selected company's on-disk VFS.
*
* @param {{ googleAuthId: number, store?: object, vfsRoot?: string }} ctx
* @returns {Promise<Array<string>>} List of loaded skill file names
@@ -140,19 +140,12 @@ export async function loadDefaultSkills({ googleAuthId, store: passedStore, vfsR
const loaded = [];
for (const skill of defaultSkills) {
try {
await store.modelvfs.write(googleAuthId, {
name: skill.name,
mime: 'text/markdown',
content: Buffer.from(skill.content, 'utf8'),
});
} catch (err) {
console.error(`[vfs] failed to write default skill "${skill.name}" to modelvfs:`, err.message);
}
try {
const diskPath = path.resolve(vfsRoot, skill.name);
fs.writeFileSync(diskPath, skill.content, 'utf8');
if (!fs.existsSync(diskPath)) {
fs.mkdirSync(path.dirname(diskPath), { recursive: true });
fs.writeFileSync(diskPath, skill.content, 'utf8');
}
loaded.push(skill.name);
} catch (err) {
console.error(`[vfs] failed to write default skill "${skill.name}" to disk:`, err.message);
@@ -178,7 +171,7 @@ export async function initCompanyVfs({ googleAuthId, store: passedStore, vfsRoot
}
/**
* Ensure the local directory for this company's VFS exists and is hydrated from modelvfs.
* Ensure the local directory for this company's VFS exists and default skills are loaded.
*
* @param {{ googleAuthId: number }} ctx
* @returns {Promise<string>} vfsRoot directory path
@@ -189,86 +182,21 @@ export async function prepareVfs({ googleAuthId }) {
fs.mkdirSync(vfsRoot, { recursive: true });
try {
const files = await store.modelvfs.list(googleAuthId);
const existingNames = new Set(files.map((f) => f.name));
const defaultSkills = getDefaultSkills();
for (const skill of defaultSkills) {
if (!existingNames.has(skill.name)) {
try {
await store.modelvfs.write(googleAuthId, {
name: skill.name,
mime: 'text/markdown',
content: Buffer.from(skill.content, 'utf8'),
});
files.push({ name: skill.name });
existingNames.add(skill.name);
} catch (err) {
console.warn(`[vfs] failed to write default skill "${skill.name}" to modelvfs:`, err.message);
}
}
}
for (const file of files) {
const diskPath = path.resolve(vfsRoot, file.name);
if (!diskPath.startsWith(vfsRoot)) continue;
if (!fs.existsSync(diskPath)) {
fs.mkdirSync(path.dirname(diskPath), { recursive: true });
const fullRecord = await store.modelvfs.read(googleAuthId, file.name);
if (fullRecord?.content) {
fs.writeFileSync(diskPath, fullRecord.content);
}
}
}
await loadDefaultSkills({ googleAuthId, store, vfsRoot });
} catch (err) {
console.warn('[vfs] hydration warning:', err.message);
console.warn('[vfs] prepare warning:', err.message);
}
return vfsRoot;
}
/**
* Syncs any modified or newly written files from the disk vfsRoot back to the modelvfs table.
* Purely disk-based VFS; no database syncing needed. Kept as no-op for backward compatibility.
*
* The store re-scopes by the currently selected company on every call, so this only stays
* correct while `vfsRoot` was produced by `prepareVfs`/`resolveVfsRoot` in the same turn.
*
* @param {{ googleAuthId: number, vfsRoot: string }} ctx
* @param {{ googleAuthId?: number, vfsRoot?: string }} [ctx]
*/
export async function syncVfsToDb({ googleAuthId, vfsRoot }) {
if (!fs.existsSync(vfsRoot)) return;
const store = await getStore();
function walk(dir, rel = '') {
const entries = fs.readdirSync(dir, { withFileTypes: true });
const list = [];
for (const e of entries) {
const relPath = rel ? `${rel}/${e.name}` : e.name;
const fullPath = path.join(dir, e.name);
if (e.isDirectory()) {
list.push(...walk(fullPath, relPath));
} else if (e.isFile()) {
list.push({ relPath, fullPath });
}
}
return list;
}
const currentFiles = walk(vfsRoot);
for (const f of currentFiles) {
if (isReadOnlyDatabasePath(f.relPath)) continue;
try {
const content = fs.readFileSync(f.fullPath);
const mime = getMimeType(f.relPath);
await store.modelvfs.write(googleAuthId, {
name: f.relPath,
mime,
content,
});
} catch (err) {
console.error(`[vfs] failed to sync file ${f.relPath} to db:`, err.message);
}
}
export async function syncVfsToDb({ googleAuthId, vfsRoot } = {}) {
// Purely disk-based VFS; no database syncing.
}
/**