[Add] Synaptic AI Pro
https://assetstore.unity.com/packages/tools/generative-ai/synaptic-ai-pro-natural-language-control-for-unity-336030
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* generate-simple-registry.js
|
||||
*
|
||||
* Generates tool-registry.json WITHOUT embeddings (no API key required)
|
||||
* Now includes inputSchema for LLM tool usage
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/generate-simple-registry.js
|
||||
*/
|
||||
|
||||
import { detectCategory } from '../utils/tool-loader.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
/**
|
||||
* Parse zod schema string into JSON Schema
|
||||
*/
|
||||
function parseZodSchema(zodString) {
|
||||
if (!zodString) return null;
|
||||
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: []
|
||||
};
|
||||
|
||||
// Match property definitions like: name: z.string().describe('...')
|
||||
// Handle nested objects and various zod methods
|
||||
const lines = zodString.split('\n');
|
||||
let braceDepth = 0;
|
||||
|
||||
for (let li = 0; li < lines.length; li++) {
|
||||
const line = lines[li];
|
||||
const trimmed = line.trim();
|
||||
|
||||
// Track brace depth for nested objects
|
||||
braceDepth += (trimmed.match(/\{/g) || []).length;
|
||||
braceDepth -= (trimmed.match(/\}/g) || []).length;
|
||||
|
||||
// Match top-level property: propertyName: z.type(...)
|
||||
const propMatch = trimmed.match(/^(\w+):\s*z\.(\w+)\(/);
|
||||
if (propMatch && braceDepth <= 1) {
|
||||
const [, propName, zodType] = propMatch;
|
||||
|
||||
// Skip 'inputSchema' itself - we're parsing its contents
|
||||
if (propName === 'inputSchema') continue;
|
||||
|
||||
const prop = {};
|
||||
|
||||
// Build "lookahead window" — if the property continues to next lines
|
||||
// (multi-line z.enum/z.union/etc), accumulate until parens balance.
|
||||
// This lets us catch .describe() that appears on a later line.
|
||||
let window = line;
|
||||
let parenDepth = (line.match(/\(/g) || []).length - (line.match(/\)/g) || []).length;
|
||||
let extraLines = 0;
|
||||
// Continue while: parens unbalanced OR next line is a chain continuation (.method)
|
||||
while ((li + extraLines + 1) < lines.length && extraLines < 30) {
|
||||
const nextLine = lines[li + extraLines + 1];
|
||||
const nextTrim = nextLine.trim();
|
||||
const isChainContinuation = nextTrim.startsWith('.');
|
||||
if (parenDepth > 0 || isChainContinuation) {
|
||||
extraLines++;
|
||||
window += '\n' + nextLine;
|
||||
parenDepth += (nextLine.match(/\(/g) || []).length;
|
||||
parenDepth -= (nextLine.match(/\)/g) || []).length;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const isOptional = /\.optional\(\)/.test(window);
|
||||
|
||||
// Extract description (single quote, double quote, or backtick)
|
||||
let descMatch = window.match(/\.describe\(\s*'((?:[^']|\\')*)'\s*\)/);
|
||||
if (!descMatch) descMatch = window.match(/\.describe\(\s*"((?:[^"]|\\")*)"\s*\)/);
|
||||
if (!descMatch) descMatch = window.match(/\.describe\(\s*`([\s\S]*?)`\s*\)/);
|
||||
if (descMatch) prop.description = descMatch[1].split('\n')[0].trim();
|
||||
|
||||
// Extract default value
|
||||
const defaultMatch = window.match(/\.default\(([^)]+)\)/);
|
||||
if (defaultMatch) {
|
||||
try {
|
||||
const defaultVal = defaultMatch[1].trim();
|
||||
if (defaultVal.startsWith("'") || defaultVal.startsWith('"')) {
|
||||
prop.default = defaultVal.slice(1, -1);
|
||||
} else if (defaultVal === 'true' || defaultVal === 'false') {
|
||||
prop.default = defaultVal === 'true';
|
||||
} else if (!isNaN(Number(defaultVal))) {
|
||||
prop.default = Number(defaultVal);
|
||||
} else {
|
||||
prop.default = defaultVal;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Map zod types to JSON Schema
|
||||
switch (zodType) {
|
||||
case 'string':
|
||||
prop.type = 'string';
|
||||
break;
|
||||
case 'number':
|
||||
prop.type = 'number';
|
||||
break;
|
||||
case 'boolean':
|
||||
prop.type = 'boolean';
|
||||
break;
|
||||
case 'enum':
|
||||
prop.type = 'string';
|
||||
const enumMatch = window.match(/z\.enum\(\[([\s\S]+?)\]\)/);
|
||||
if (enumMatch) {
|
||||
prop.enum = enumMatch[1].split(',')
|
||||
.map(s => s.trim().replace(/['"`]/g, ''))
|
||||
.filter(s => s.length > 0);
|
||||
}
|
||||
break;
|
||||
case 'object':
|
||||
prop.type = 'object';
|
||||
if (window.includes('x: z.number()') || window.includes('x:z.number()')) {
|
||||
prop.properties = {
|
||||
x: { type: 'number' },
|
||||
y: { type: 'number' },
|
||||
z: { type: 'number' }
|
||||
};
|
||||
}
|
||||
break;
|
||||
case 'array':
|
||||
prop.type = 'array';
|
||||
break;
|
||||
case 'union':
|
||||
prop.type = 'string';
|
||||
break;
|
||||
default:
|
||||
prop.type = 'string';
|
||||
}
|
||||
|
||||
schema.properties[propName] = prop;
|
||||
|
||||
if (!isOptional && !defaultMatch) {
|
||||
schema.required.push(propName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove empty required array
|
||||
if (schema.required.length === 0) {
|
||||
delete schema.required;
|
||||
}
|
||||
|
||||
return Object.keys(schema.properties).length > 0 ? schema : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract inputSchema block from tool definition
|
||||
*/
|
||||
function extractInputSchema(content, toolStartIndex) {
|
||||
// Find inputSchema: z.object({ starting from toolStartIndex
|
||||
const searchArea = content.substring(toolStartIndex, toolStartIndex + 3000);
|
||||
const schemaStart = searchArea.indexOf('inputSchema: z.object({');
|
||||
|
||||
if (schemaStart === -1) return null;
|
||||
|
||||
// Find matching closing brace
|
||||
let braceCount = 0;
|
||||
let inSchema = false;
|
||||
let schemaEnd = schemaStart;
|
||||
|
||||
for (let i = schemaStart; i < searchArea.length; i++) {
|
||||
const char = searchArea[i];
|
||||
if (char === '{') {
|
||||
braceCount++;
|
||||
inSchema = true;
|
||||
} else if (char === '}') {
|
||||
braceCount--;
|
||||
if (inSchema && braceCount === 0) {
|
||||
schemaEnd = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const schemaBlock = searchArea.substring(schemaStart, schemaEnd);
|
||||
return parseZodSchema(schemaBlock);
|
||||
}
|
||||
|
||||
// Load existing index.js to extract tool definitions
|
||||
function extractToolsFromIndexJs() {
|
||||
const indexPath = path.join(__dirname, '..', 'index.js');
|
||||
const content = fs.readFileSync(indexPath, 'utf-8');
|
||||
|
||||
const tools = [];
|
||||
|
||||
// Find all registerTool calls with their positions
|
||||
const registerRegex = /mcpServer\.registerTool\('([^']+)',\s*\{/g;
|
||||
let match;
|
||||
|
||||
while ((match = registerRegex.exec(content)) !== null) {
|
||||
const name = match[1];
|
||||
const startIndex = match.index;
|
||||
|
||||
// Extract title and description from the block after this match
|
||||
const blockArea = content.substring(startIndex, startIndex + 2000);
|
||||
|
||||
// Title
|
||||
const titleMatch = blockArea.match(/title:\s*'([^']+)'/);
|
||||
const title = titleMatch ? titleMatch[1] : name;
|
||||
|
||||
// Description (single quote or backtick)
|
||||
let description = '';
|
||||
const descSingleMatch = blockArea.match(/description:\s*'([^']+)'/);
|
||||
const descBacktickMatch = blockArea.match(/description:\s*`([^`]+)`/);
|
||||
|
||||
if (descSingleMatch) {
|
||||
description = descSingleMatch[1].substring(0, 500);
|
||||
} else if (descBacktickMatch) {
|
||||
description = descBacktickMatch[1].split('\n')[0].trim().substring(0, 500);
|
||||
}
|
||||
|
||||
// Extract inputSchema
|
||||
const inputSchema = extractInputSchema(content, startIndex);
|
||||
|
||||
tools.push({
|
||||
name,
|
||||
title,
|
||||
description,
|
||||
inputSchema
|
||||
});
|
||||
}
|
||||
|
||||
// Remove duplicates
|
||||
const uniqueTools = [];
|
||||
const seenNames = new Set();
|
||||
for (const tool of tools) {
|
||||
if (!seenNames.has(tool.name)) {
|
||||
seenNames.add(tool.name);
|
||||
uniqueTools.push(tool);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Generator] Found ${uniqueTools.length} tools in index.js`);
|
||||
return uniqueTools;
|
||||
}
|
||||
|
||||
function generateRegistry() {
|
||||
console.log('[Generator] Starting simple tool registry generation (with inputSchema)...');
|
||||
|
||||
const tools = extractToolsFromIndexJs();
|
||||
|
||||
if (tools.length === 0) {
|
||||
console.error('[Generator] ERROR: No tools found in index.js');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const registry = {};
|
||||
let schemaCount = 0;
|
||||
|
||||
for (const tool of tools) {
|
||||
// Detect category
|
||||
const category = detectCategory(tool.name);
|
||||
|
||||
registry[tool.name] = {
|
||||
title: tool.title,
|
||||
description: tool.description,
|
||||
category: category,
|
||||
inputSchema: tool.inputSchema || null,
|
||||
embedding: null
|
||||
};
|
||||
|
||||
if (tool.inputSchema) {
|
||||
schemaCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Write to file
|
||||
const outputPath = path.join(__dirname, '..', 'tool-registry.json');
|
||||
fs.writeFileSync(outputPath, JSON.stringify(registry, null, 2));
|
||||
|
||||
console.log(`[Generator] ✅ Successfully generated tool-registry.json`);
|
||||
console.log(`[Generator] Location: ${outputPath}`);
|
||||
console.log(`[Generator] Total tools: ${Object.keys(registry).length}`);
|
||||
console.log(`[Generator] Tools with inputSchema: ${schemaCount}`);
|
||||
|
||||
// Category breakdown
|
||||
const categoryCount = {};
|
||||
for (const meta of Object.values(registry)) {
|
||||
categoryCount[meta.category] = (categoryCount[meta.category] || 0) + 1;
|
||||
}
|
||||
|
||||
console.log('[Generator] Category breakdown:');
|
||||
for (const [category, count] of Object.entries(categoryCount).sort((a, b) => b[1] - a[1])) {
|
||||
console.log(` ${category}: ${count} tools`);
|
||||
}
|
||||
}
|
||||
|
||||
generateRegistry();
|
||||
Reference in New Issue
Block a user