c9ai
Version:
Universal AI assistant with vibe-based workflows, hybrid cloud+local AI, and comprehensive tool integration
240 lines (210 loc) • 6.75 kB
JavaScript
;
/**
* Function Registry - File-based storage for user-defined functions
* Uses .xmlp files for XML-Lisp source and .js files for transpiled code
*/
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
class FunctionRegistry {
constructor() {
this.functionsDir = path.join(os.homedir(), '.c9ai', 'functions');
this.registryPath = path.join(this.functionsDir, 'registry.json');
this.ensureFunctionsDir();
this.functions = this.loadRegistry();
}
/**
* Ensure functions directory exists
*/
ensureFunctionsDir() {
if (!fs.existsSync(this.functionsDir)) {
fs.mkdirSync(this.functionsDir, { recursive: true });
console.log(`📁 Created functions directory: ${this.functionsDir}`);
}
}
/**
* Load function registry from disk
*/
loadRegistry() {
try {
if (fs.existsSync(this.registryPath)) {
const data = JSON.parse(fs.readFileSync(this.registryPath, 'utf8'));
console.log(`📚 Loaded ${Object.keys(data).length} user-defined functions`);
return data;
}
} catch (error) {
console.log(`Failed to load function registry: ${error.message}`);
}
return {};
}
/**
* Get file paths for a function
*/
getFunctionPaths(name) {
return {
xmlp: path.join(this.functionsDir, `${name}.xmlp`),
js: path.join(this.functionsDir, `${name}.js`),
tempXmlp: path.join(this.functionsDir, `${name}.xmlp.tmp`),
tempJs: path.join(this.functionsDir, `${name}.js.tmp`)
};
}
/**
* Save function registry to disk
*/
saveRegistry() {
try {
fs.writeFileSync(this.registryPath, JSON.stringify(this.functions, null, 2));
console.log(`💾 Saved ${Object.keys(this.functions).length} functions to registry`);
} catch (error) {
console.log(`Failed to save function registry: ${error.message}`);
}
}
/**
* Register a new function with atomic file operations
*/
async registerFunction(name, javascript, xmlSource = null) {
const paths = this.getFunctionPaths(name);
try {
// 1. Write XML-Lisp source to temp file
if (xmlSource) {
await fs.promises.writeFile(paths.tempXmlp, xmlSource, 'utf8');
}
// 2. Write JavaScript to temp file
await fs.promises.writeFile(paths.tempJs, javascript, 'utf8');
// 3. Test the JavaScript function (basic syntax check)
try {
const testCode = `${javascript}; if (typeof ${name} === 'function') console.log('OK'); else throw new Error('Not a function');`;
require('vm').runInNewContext(testCode);
} catch (testError) {
throw new Error(`Function test failed: ${testError.message}`);
}
// 4. Atomic moves (rename is atomic on most filesystems)
if (xmlSource) {
await fs.promises.rename(paths.tempXmlp, paths.xmlp);
}
await fs.promises.rename(paths.tempJs, paths.js);
// 5. Update registry metadata
this.functions[name] = {
created: this.functions[name]?.created || Date.now(),
lastUsed: Date.now(),
status: 'active'
};
this.saveRegistry();
console.log(`✅ Registered function: ${name}`);
} catch (error) {
// Clean up temp files on failure
try {
if (fs.existsSync(paths.tempXmlp)) fs.unlinkSync(paths.tempXmlp);
if (fs.existsSync(paths.tempJs)) fs.unlinkSync(paths.tempJs);
} catch (cleanupError) {
console.log(`Warning: Failed to clean up temp files: ${cleanupError.message}`);
}
console.log(`❌ Failed to register function ${name}: ${error.message}`);
throw error;
}
}
/**
* Get a function's JavaScript code from file
*/
getFunction(name) {
if (!this.hasFunction(name)) {
return null;
}
try {
const paths = this.getFunctionPaths(name);
const javascript = fs.readFileSync(paths.js, 'utf8');
// Update last used timestamp
this.functions[name].lastUsed = Date.now();
this.saveRegistry();
return javascript;
} catch (error) {
console.log(`Failed to read function ${name}: ${error.message}`);
return null;
}
}
/**
* Get XML-Lisp source for a function
*/
getXMLSource(name) {
if (!this.hasFunction(name)) {
return null;
}
try {
const paths = this.getFunctionPaths(name);
if (fs.existsSync(paths.xmlp)) {
return fs.readFileSync(paths.xmlp, 'utf8');
}
} catch (error) {
console.log(`Failed to read XML source for ${name}: ${error.message}`);
}
return null;
}
/**
* Check if function exists
*/
hasFunction(name) {
return name in this.functions && this.functions[name].status === 'active';
}
/**
* Get all registered function names
*/
getFunctionNames() {
return Object.keys(this.functions).filter(name =>
this.functions[name].status === 'active'
);
}
/**
* Get injectable code for all functions
*/
getInjectableCode() {
let code = '';
for (const name of this.getFunctionNames()) {
const jsCode = this.getFunction(name);
if (jsCode) {
code += jsCode + '\n\n';
}
}
return code;
}
/**
* Get functions as context object assignments
*/
getContextAssignments() {
let code = '';
for (const name of this.getFunctionNames()) {
const jsCode = this.getFunction(name);
if (jsCode) {
// Convert function declaration to assignment
const functionMatch = jsCode.match(/function\s+(\w+)\s*\(([^)]*)\)\s*\{([\s\S]*)\}/);
if (functionMatch) {
const [, funcName, params, body] = functionMatch;
code += ` ${funcName}: (${params}) => {${body}},\n`;
}
}
}
return code;
}
/**
* Get function metadata with file info
*/
getFunctionInfo(name) {
if (!this.hasFunction(name)) {
return null;
}
const metadata = this.functions[name];
const xmlSource = this.getXMLSource(name);
const jsSource = this.getFunction(name);
return {
name,
created: metadata.created,
lastUsed: metadata.lastUsed,
status: metadata.status,
hasXMLSource: !!xmlSource,
xmlSource,
jsSource
};
}
}
// Global instance
const globalRegistry = new FunctionRegistry();
module.exports = { FunctionRegistry, globalRegistry };