Initial commit
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user