u
This commit is contained in:
@@ -276,7 +276,7 @@ export function createAnalyzeImageTool({ googleAuthId, model }) {
|
||||
return defineTool({
|
||||
name: 'analyze_image',
|
||||
description:
|
||||
'Inspect 1–9 images via vision model. Multiple paths render as a labelled comparison grid. ' +
|
||||
'Inspect 1-9 images via vision model. Multiple paths render as a labelled comparison grid. ' +
|
||||
'Paths: VFS-relative ("images/foo.avif") or DB ("image/<hash>"). Returns model evaluation.',
|
||||
strict: false,
|
||||
parameters: {
|
||||
@@ -379,12 +379,12 @@ export function createAnalyzeImageTool({ googleAuthId, model }) {
|
||||
const question = typeof prompt === 'string' && prompt.trim() ? prompt.trim() : null;
|
||||
const text = items.length > 1
|
||||
? `${question || 'Compare these images, judge visual quality, flag watermarks, text overlays or defects, and say which one is best and why.'}\n\n` +
|
||||
`The grid is ${shape.name}. Each image has its file name printed beneath it:\n` +
|
||||
items.map((it, idx) => {
|
||||
const row = Math.floor(idx / GRID_CONFIGS[shape.name].cols) + 1;
|
||||
const col = (idx % GRID_CONFIGS[shape.name].cols) + 1;
|
||||
return `- Row ${row}, Col ${col}: "${it.name}"`;
|
||||
}).join('\n')
|
||||
`The grid is ${shape.name}. Each image has its file name printed beneath it:\n` +
|
||||
items.map((it, idx) => {
|
||||
const row = Math.floor(idx / GRID_CONFIGS[shape.name].cols) + 1;
|
||||
const col = (idx % GRID_CONFIGS[shape.name].cols) + 1;
|
||||
return `- Row ${row}, Col ${col}: "${it.name}"`;
|
||||
}).join('\n')
|
||||
: question || 'Describe this image, judge its visual quality, flag any watermark, text overlay or defect, and say whether it is usable as a product or category image.';
|
||||
|
||||
const apiKey = await resolveOpenRouterKey(googleAuthId);
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { defineTool, ToolError } from 'libllmorbiter';
|
||||
import { isSkillPath, parseSkill, SKILL_EXT } from './skill.js';
|
||||
import { getApiRouteSkill } from './routes-skills.js';
|
||||
import { prepareVfs } from './vfs.js';
|
||||
|
||||
const DEFAULT_LIMIT = 400;
|
||||
@@ -109,17 +110,18 @@ export function createReadFileTool({ googleAuthId }) {
|
||||
throw new ToolError(`Access denied: path "${filename}" escapes virtual filesystem boundary.`);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(absPath)) {
|
||||
const virtualSkill = !fs.existsSync(absPath) && isSkillPath(clean) ? getApiRouteSkill(clean) : null;
|
||||
if (!virtualSkill && !fs.existsSync(absPath)) {
|
||||
throw new ToolError(
|
||||
`File not found in virtual filesystem: "${clean}". ` +
|
||||
'Call find_files to see what is stored, or find_files({ skills: true }) for SKILL files.'
|
||||
);
|
||||
}
|
||||
if (!fs.statSync(absPath).isFile()) {
|
||||
if (!virtualSkill && !fs.statSync(absPath).isFile()) {
|
||||
throw new ToolError(`"${clean}" is not a file. Use find_files to list directories.`);
|
||||
}
|
||||
|
||||
const raw = fs.readFileSync(absPath);
|
||||
const raw = virtualSkill ? Buffer.from(virtualSkill.content, 'utf8') : fs.readFileSync(absPath);
|
||||
if (looksBinary(raw)) {
|
||||
const ext = path.extname(clean).toLowerCase();
|
||||
throw new ToolError(
|
||||
|
||||
114
src/tools/routes-skills.js
Normal file
114
src/tools/routes-skills.js
Normal file
@@ -0,0 +1,114 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
/**
|
||||
* routes-skills.js — virtual SKILL files generated from docs/api/routes.md.
|
||||
*
|
||||
* routes.md is split into `### Section: /<segment>` blocks by scripts/generateApiDocs.js. Each
|
||||
* block becomes one in-memory `api-<segment>.SKILL` file (never written to disk), so the model
|
||||
* reads only the routes it needs. Notation and the reusable `Tn` types a section references
|
||||
* (transitively) are inlined so every skill is self-contained.
|
||||
*
|
||||
* Nothing here imports skill.js (skill.js imports this module); the SKILL format is
|
||||
* "title on line 1, blank line, markdown".
|
||||
*/
|
||||
|
||||
const ROUTES_DOC_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../docs/api/routes.md');
|
||||
|
||||
let cache = { mtimeMs: -1, skills: [] };
|
||||
|
||||
function slugify(text) {
|
||||
return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'root';
|
||||
}
|
||||
|
||||
function parseRoutesDoc(text) {
|
||||
const lines = text.split('\n');
|
||||
let notation = [];
|
||||
const types = new Map();
|
||||
const sections = [];
|
||||
let mode = null; // 'notation' | 'types' | 'section' | null
|
||||
let current = null;
|
||||
|
||||
for (const line of lines) {
|
||||
const heading = /^### (.*)$/.exec(line);
|
||||
if (heading) {
|
||||
const h = heading[1].trim();
|
||||
current = null;
|
||||
if (h === 'Notation') {
|
||||
mode = 'notation';
|
||||
} else if (h === 'Reusable types') {
|
||||
mode = 'types';
|
||||
} else if (h === 'Routes') {
|
||||
mode = null;
|
||||
} else if (h.startsWith('Section: ')) {
|
||||
const name = h.slice('Section: '.length).trim();
|
||||
current = { slug: slugify(name), title: `API routes: ${name}`, lines: [] };
|
||||
sections.push(current);
|
||||
mode = 'section';
|
||||
} else {
|
||||
const name = h.replace(/\s*\(.*$/, '');
|
||||
current = { slug: slugify(name), title: `API routes: ${name}`, lines: [line] };
|
||||
sections.push(current);
|
||||
mode = 'section';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (mode === 'notation') notation.push(line);
|
||||
else if (mode === 'types') {
|
||||
const m = /^- `(T\d+)` = `(.*)`$/.exec(line);
|
||||
if (m) types.set(m[1], m[2]);
|
||||
} else if (mode === 'section' && current) current.lines.push(line);
|
||||
}
|
||||
notation = notation.join('\n').trim();
|
||||
return { notation, types, sections };
|
||||
}
|
||||
|
||||
function buildSkills(text) {
|
||||
const { notation, types, sections } = parseRoutesDoc(text);
|
||||
return sections.map((section) => {
|
||||
const body = section.lines.join('\n').trim();
|
||||
const used = new Set();
|
||||
const queue = [...body.matchAll(/\bT\d+\b/g)].map((m) => m[0]);
|
||||
while (queue.length) {
|
||||
const id = queue.pop();
|
||||
if (used.has(id) || !types.has(id)) continue;
|
||||
used.add(id);
|
||||
queue.push(...[...types.get(id).matchAll(/\bT\d+\b/g)].map((m) => m[0]));
|
||||
}
|
||||
const typeLines = [...used]
|
||||
.sort((a, b) => Number(a.slice(1)) - Number(b.slice(1)))
|
||||
.map((id) => `- \`${id}\` = \`${types.get(id)}\``);
|
||||
|
||||
const parts = [section.title, '', '### Notation', '', notation];
|
||||
if (typeLines.length) parts.push('', '### Reusable types', '', ...typeLines);
|
||||
parts.push('', '### Routes', '', body, '');
|
||||
return { name: `api-${section.slug}.SKILL`, title: section.title, content: parts.join('\n') };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual API-route skills, regenerated whenever routes.md changes on disk.
|
||||
* @returns {Array<{ name: string, title: string, content: string }>}
|
||||
*/
|
||||
export function getApiRouteSkills() {
|
||||
try {
|
||||
const { mtimeMs } = fs.statSync(ROUTES_DOC_PATH);
|
||||
if (mtimeMs !== cache.mtimeMs) {
|
||||
cache = { mtimeMs, skills: buildSkills(fs.readFileSync(ROUTES_DOC_PATH, 'utf8')) };
|
||||
}
|
||||
return cache.skills;
|
||||
} catch (err) {
|
||||
console.error('routes-skills: failed to read routes doc:', err.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name bare file name, e.g. `api-article.SKILL`
|
||||
* @returns {{ name: string, title: string, content: string }|null}
|
||||
*/
|
||||
export function getApiRouteSkill(name) {
|
||||
const lower = String(name).toLowerCase();
|
||||
return getApiRouteSkills().find((s) => s.name.toLowerCase() === lower) || null;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { getApiRouteSkills } from './routes-skills.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -152,6 +153,19 @@ export function readSkillTitle(absPath) {
|
||||
* @returns {Array<{ path: string, title: string, lines: number, size: number, updated: string|null }>}
|
||||
*/
|
||||
export function listSkillFiles(vfsRoot) {
|
||||
const out = listDiskSkillFiles(vfsRoot);
|
||||
const onDisk = new Set(out.map((s) => s.path.toLowerCase()));
|
||||
for (const v of getApiRouteSkills()) {
|
||||
if (onDisk.has(v.name.toLowerCase())) continue;
|
||||
const split = v.content.split('\n');
|
||||
if (split[split.length - 1] === '') split.pop();
|
||||
out.push({ path: v.name, title: v.title, lines: split.length, size: Buffer.byteLength(v.content), updated: null, virtual: true });
|
||||
}
|
||||
out.sort((a, b) => a.path.localeCompare(b.path));
|
||||
return out;
|
||||
}
|
||||
|
||||
function listDiskSkillFiles(vfsRoot) {
|
||||
const out = [];
|
||||
if (!vfsRoot || !fs.existsSync(vfsRoot)) return out;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user