dt_mcp.js•5.13 kB
// dt_mcp.js
import { promises as fs } from 'fs';
/**
* Asynchronous parser for .rtdq files (v2.1 format).
* Throws an error if the file cannot be read or parsed.
*/
async function parseRTDQFile(filePath) {
if (typeof filePath !== 'string' || !filePath.endsWith('.rtdq')) {
throw new Error('Invalid file path or extension.');
}
const text = await fs.readFile(filePath, 'utf-8');
const data = {
rtdq_name: '',
description: '',
suggested_tools: [],
tasks: [],
missing_info: [],
};
let lines = text.split(/\r?\n/).map(l => l.trim());
let idx = 0;
let currentTask = null;
let currentNode = null;
let inSection = null; // null | 'DESCRIPTION' | 'SUGGESTED_TOOLS' | 'MISSING_INFO'
while (idx < lines.length) {
const line = lines[idx];
if (line.startsWith('BEGIN_RTDQ:')) {
data.rtdq_name = line.replace('BEGIN_RTDQ:', '').trim();
inSection = null;
} else if (line.startsWith('END_RTDQ')) {
break;
} else if (line.startsWith('DESCRIPTION:')) {
inSection = 'DESCRIPTION';
// Expecting single quoted line immediately after for simplicity here
if (lines[idx + 1]?.startsWith('"') && lines[idx + 1]?.endsWith('"')) {
data.description = lines[idx + 1].slice(1, -1);
idx++; // Skip description line
}
} else if (line.startsWith('SUGGESTED_TOOLS:')) {
inSection = 'SUGGESTED_TOOLS';
data.suggested_tools = [];
} else if (line.startsWith('TASKS:')) {
inSection = null;
} else if (line.startsWith('MISSING_INFO:')) {
inSection = 'MISSING_INFO';
data.missing_info = [];
} else if (inSection === 'SUGGESTED_TOOLS' && line.startsWith('- ')) {
const toolMatch = line.match(/^\-\s*"(.*)"$/);
if (toolMatch) data.suggested_tools.push(toolMatch[1]);
} else if (inSection === 'MISSING_INFO' && line.startsWith('- ')) {
const missingMatch = line.match(/^\-\s*"(.*)"$/);
if (missingMatch) data.missing_info.push(missingMatch[1]);
} else if (line === 'BEGIN_TASK') {
currentTask = { name: '', purpose: '', nodes: [] };
inSection = null;
} else if (line.startsWith('END_TASK') && currentTask) {
data.tasks.push(currentTask);
currentTask = null;
} else if (currentTask && !currentNode && line.startsWith('NAME:')) {
currentTask.name = line.replace('NAME:', '').trim();
} else if (currentTask && !currentNode && line.startsWith('PURPOSE:')) {
const purposeMatch = line.match(/^PURPOSE:\s*"(.*)"$/);
if (purposeMatch) currentTask.purpose = purposeMatch[1];
} else if (currentTask && line === 'BEGIN_DT') {
// Context marker
} else if (currentTask && line === 'END_DT') {
// Context marker
} else if (currentTask && line.startsWith('NODE:')) {
currentNode = { id: '', prompt: '', children: [] };
} else if (currentNode && line.startsWith('END_NODE')) {
if (currentTask) currentTask.nodes.push(currentNode);
currentNode = null;
} else if (currentNode && line.startsWith('ID:')) {
currentNode.id = line.replace('ID:', '').trim();
} else if (currentNode && line.startsWith('PROMPT:')) {
const promptMatch = line.match(/^\s*PROMPT:\s*"(.*)"$/);
if (promptMatch) currentNode.prompt = promptMatch[1];
} else if (currentNode && line.startsWith('CHILD:')) {
idx++;
let answer = '', nextID = '';
// Note: A* cost/heuristic parsing could be added here based on Story #11
while (idx < lines.length && !lines[idx].match(/^\s*(CHILD:|END_NODE)/)) {
const childLine = lines[idx].trim();
if (childLine.startsWith('ANSWER:')) {
const answerMatch = childLine.match(/^ANSWER:\s*"(.*)"$/);
if (answerMatch) answer = answerMatch[1];
} else if (childLine.startsWith('NEXT:')) {
nextID = childLine.replace('NEXT:', '').trim();
}
idx++;
}
if (answer && nextID) currentNode.children.push({ answer, next: nextID });
idx--;
}
if (line === 'BEGIN_TASK' || line.startsWith('NODE:')) {
inSection = null;
}
idx++;
}
if (!data.rtdq_name) {
throw new Error('Parsing failed: Missing BEGIN_RTDQ marker or name.');
}
return data;
}
/**
* Helper to find a node by ID within a specific task in the parsed data.
*/
function getNodeById(rtdqData, taskName, nodeId) {
if (!rtdqData || !rtdqData.tasks) return null;
const task = rtdqData.tasks.find(t => t.name === taskName);
if (!task || !task.nodes) return null;
return task.nodes.find(n => n.id === nodeId) || null;
}
export { parseRTDQFile, getNodeById };