166 lines
4.6 KiB
JavaScript
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);
|
|
}
|