Files
agent-session-manager/backend/src/tools.js
T
2026-07-16 15:42:50 +08:00

166 lines
4.6 KiB
JavaScript

import { readdirSync, existsSync, readFileSync } from 'fs';
import { join } from 'path';
import { getCodexHome } from './db.js';
const CODEX_HOME = getCodexHome();
/**
* Parse YAML frontmatter (--- delimited) from a file.
*/
function parseFrontmatter(content) {
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
if (!match) return null;
const yaml = match[1];
const meta = {};
for (const line of yaml.split('\n')) {
const kv = line.match(/^(\w+):\s*(.*\S)?\s*$/);
if (kv) {
meta[kv[1]] = kv[2] || '';
}
}
return meta;
}
/**
* Scan local tools/skills/plugins available to Codex.
* This is a static scan of the .codex directory for registered tools.
*/
export function scanAvailableTools() {
const tools = {
builtin: [],
skills: [],
plugins: [],
cache_tools: [],
};
// Scan skills directory
const skillsDir = join(CODEX_HOME, 'skills');
if (existsSync(skillsDir)) {
try {
const entries = readdirSync(skillsDir);
for (const entry of entries) {
const skillPath = join(skillsDir, entry);
const meta = extractSkillMeta(skillPath);
tools.skills.push({
name: entry,
path: skillPath,
description: meta?.description || meta?.name || '',
is_dir: true,
});
}
} catch (e) {
console.warn('[Tools] Skills scan error:', e.message);
}
}
// Scan plugins directory
const pluginsDir = join(CODEX_HOME, 'plugins');
if (existsSync(pluginsDir)) {
try {
const entries = readdirSync(pluginsDir);
for (const entry of entries) {
tools.plugins.push({
name: entry,
path: join(pluginsDir, entry),
});
}
} catch (e) {
console.warn('[Tools] Plugins scan error:', e.message);
}
}
// Scan cache/tools for JSON tool definitions
const cacheToolsDir = join(CODEX_HOME, 'cache', 'codex_apps_tools');
if (existsSync(cacheToolsDir)) {
try {
const entries = readdirSync(cacheToolsDir);
for (const entry of entries) {
if (entry.endsWith('.json')) {
try {
const content = readFileSync(join(cacheToolsDir, entry), 'utf-8');
const data = JSON.parse(content);
// Flatten if it's a wrapping object like { tools: [...] }
if (data && !Array.isArray(data) && data.tools && Array.isArray(data.tools)) {
tools.cache_tools.push({
file: entry,
description: data.description || '',
tool_names: data.tools.map(t => t.name || t.function?.name || 'unnamed'),
});
} else {
const arr = Array.isArray(data) ? data : [data];
tools.cache_tools.push({
file: entry,
description: '',
tool_names: arr.map(t => t.name || t.function?.name || 'unnamed'),
});
}
} catch (e) {
// skip unparseable
}
}
}
} catch (e) {
console.warn('[Tools] Cache tools scan error:', e.message);
}
}
return tools;
}
function extractSkillMeta(skillPath) {
try {
const files = readdirSync(skillPath);
// Try SKILL.md frontmatter first
const readmeFile = files.find(f => f === 'SKILL.md' || f === 'README.md');
if (readmeFile) {
const content = readFileSync(join(skillPath, readmeFile), 'utf-8');
const meta = parseFrontmatter(content);
if (meta) return meta;
}
// Fallback to JSON meta files
const metaFile = files.find(f => f === 'skill.json' || f === 'meta.json' || f === 'package.json');
if (metaFile) {
const content = readFileSync(join(skillPath, metaFile), 'utf-8');
return JSON.parse(content);
}
} catch (e) {
// ignore
}
return null;
}
/**
* Aggregate tool usage statistics across all cached sessions.
* @param {Array} sessionsCache
*/
export function aggregateToolUsage(sessionsCache) {
const toolMap = new Map();
for (const session of sessionsCache) {
if (!session.tool_calls) continue;
for (const [toolName, count] of Object.entries(session.tool_calls)) {
if (!toolMap.has(toolName)) {
toolMap.set(toolName, {
name: toolName,
total_calls: 0,
session_count: 0,
sessions: [],
});
}
const entry = toolMap.get(toolName);
entry.total_calls += count;
entry.session_count++;
entry.sessions.push({
session_id: session.session_id,
calls: count,
preview: session.first_user_message?.slice(0, 80) || '',
});
}
}
return Array.from(toolMap.values())
.sort((a, b) => b.total_calls - a.total_calls);
}