UNPKG

roam-research-mcp

Version:
371 lines (370 loc) • 11.9 kB
/** * Translator: converts CLI-style commands to Roam batch actions */ import { resolveParentRef, generatePlaceholder } from './resolver.js'; import { sanitizeTagName } from '../../utils/helpers.js'; /** * Translate a single command to batch actions * May return multiple actions (e.g., table, outline) */ export function translateCommand(command, context) { switch (command.command) { case 'create': return translateCreate(command, context); case 'update': return translateUpdate(command); case 'delete': return translateDelete(command); case 'move': return translateMove(command); case 'todo': return translateTodo(command, context); case 'table': return translateTable(command, context); case 'outline': return translateOutline(command, context); case 'remember': return translateRemember(command, context); case 'page': return translatePage(command, context); case 'codeblock': return translateCodeblock(command, context); default: throw new Error(`Unknown command: ${command.command}`); } } /** * Resolve parent UID from command params */ function getParentUid(params, context) { // Direct parent UID or placeholder if (params.parent) { const resolved = resolveParentRef(params.parent, context); if (resolved) return resolved; throw new Error(`Parent "${params.parent}" could not be resolved to a UID. Page may not exist.`); } // Page UID if (params.pageUid) { return params.pageUid; } // Page title -> resolved UID if (params.page) { const uid = context.pageUids.get(params.page); if (!uid) { throw new Error(`Page "${params.page}" not found`); } return uid; } // Default to daily page if (context.dailyPageUid) { return context.dailyPageUid; } throw new Error('No parent specified and daily page not resolved'); } /** * Register a placeholder in the context */ function registerPlaceholder(name, context) { const placeholder = generatePlaceholder(name); context.placeholders.set(name, placeholder); return placeholder; } /** * Update level stack for hierarchy tracking */ function updateLevelStack(level, uid, context) { // Ensure stack is long enough while (context.levelStack.length < level) { context.levelStack.push(''); } context.levelStack[level - 1] = uid; // Truncate stack above current level context.levelStack.length = level; } /** * Get parent from level stack for hierarchical nesting */ function getParentFromLevel(level, context) { if (level <= 1) { return context.currentParent; } const parentLevel = level - 1; if (parentLevel <= context.levelStack.length && context.levelStack[parentLevel - 1]) { return context.levelStack[parentLevel - 1]; } return context.currentParent; } // --- Command translators --- function translateCreate(cmd, context) { const { params } = cmd; let parentUid; // Handle level-based hierarchy if (params.level !== undefined && params.level > 1) { const levelParent = getParentFromLevel(params.level, context); if (levelParent) { parentUid = levelParent; } else { parentUid = getParentUid(params, context); } } else { parentUid = getParentUid(params, context); // Set as current parent for level-based children if (params.level === 1 || params.level === undefined) { context.currentParent = parentUid; } } const action = { action: 'create-block', string: params.text, location: { 'parent-uid': parentUid, order: params.order ?? 'last' } }; // Register placeholder if 'as' is specified if (params.as) { action.uid = registerPlaceholder(params.as, context); } // Optional properties if (params.heading) { action.heading = params.heading; } if (params['children-view-type']) { action['children-view-type'] = params['children-view-type']; } // Update level stack if level is specified if (params.level !== undefined && params.as) { updateLevelStack(params.level, `{{uid:${params.as}}}`, context); } return [action]; } function translateUpdate(cmd) { const { params } = cmd; const action = { action: 'update-block', uid: params.uid, string: params.text }; if (params.heading !== undefined) { action.heading = params.heading; } if (params.open !== undefined) { action.open = params.open; } if (params['text-align']) { action['text-align'] = params['text-align']; } if (params['children-view-type']) { action['children-view-type'] = params['children-view-type']; } return [action]; } function translateDelete(cmd) { return [{ action: 'delete-block', uid: cmd.params.uid }]; } function translateMove(cmd) { const { params } = cmd; return [{ action: 'move-block', uid: params.uid, location: { 'parent-uid': params.parent, order: params.order ?? 'last' } }]; } function translateTodo(cmd, context) { const { params } = cmd; const parentUid = getParentUid(params, context); const action = { action: 'create-block', string: `{{[[TODO]]}} ${params.text}`, location: { 'parent-uid': parentUid, order: params.order ?? 'last' } }; if (params.as) { action.uid = registerPlaceholder(params.as, context); } return [action]; } function translateTable(cmd, context) { const { params } = cmd; const parentUid = getParentUid(params, context); const actions = []; // Table container const tableContainerPlaceholder = params.as ? registerPlaceholder(params.as, context) : registerPlaceholder(`_table_${Date.now()}`, context); actions.push({ action: 'create-block', uid: tableContainerPlaceholder, string: '{{[[table]]}}', location: { 'parent-uid': parentUid, order: params.order ?? 'last' } }); // Create columns (headers) const columnPlaceholders = []; for (let i = 0; i < params.headers.length; i++) { const colPlaceholder = registerPlaceholder(`_col_${i}_${Date.now()}`, context); columnPlaceholders.push(colPlaceholder); actions.push({ action: 'create-block', uid: colPlaceholder, string: params.headers[i] || ' ', location: { 'parent-uid': tableContainerPlaceholder, order: i } }); } // Create rows under each column for (let rowIdx = 0; rowIdx < params.rows.length; rowIdx++) { const row = params.rows[rowIdx]; // First column gets the row label actions.push({ action: 'create-block', string: row.label || ' ', location: { 'parent-uid': columnPlaceholders[0], order: rowIdx } }); // Remaining columns get the cells for (let cellIdx = 0; cellIdx < row.cells.length; cellIdx++) { actions.push({ action: 'create-block', string: row.cells[cellIdx] || ' ', location: { 'parent-uid': columnPlaceholders[cellIdx + 1], order: rowIdx } }); } } return actions; } function translateOutline(cmd, context) { const { params } = cmd; const parentUid = getParentUid(params, context); const actions = []; // Reset level stack with outline parent context.currentParent = parentUid; context.levelStack = []; for (const item of params.items) { let itemParentUid; if (item.level === 1) { itemParentUid = parentUid; } else { const levelParent = getParentFromLevel(item.level, context); if (!levelParent) { throw new Error(`Invalid outline hierarchy: level ${item.level} has no parent`); } itemParentUid = levelParent; } const action = { action: 'create-block', string: item.text, location: { 'parent-uid': itemParentUid, order: 'last' } }; // Register placeholder const placeholderName = item.as || `_outline_${actions.length}_${Date.now()}`; action.uid = registerPlaceholder(placeholderName, context); if (item.heading) { action.heading = item.heading; } // Update level stack updateLevelStack(item.level, action.uid, context); actions.push(action); } return actions; } function translateRemember(cmd, context) { const { params } = cmd; // Build memory text with categories (strip any pre-existing # or [[ ]] wrappers) let memoryText = params.text; if (params.categories && params.categories.length > 0) { const tags = params.categories.map(raw => { const cat = sanitizeTagName(raw); return `#[[${cat}]]`; }).join(' '); memoryText = `${params.text} ${tags}`; } // Add ROAM_MEMORIES_TAG if configured (we'll handle this in the CLI command) // For now, just create the block let parentUid; // If heading is specified, we'd need to look it up or create it // For simplicity, require parent UID when heading is used // TODO: Add heading resolution support if (params.heading && !params.parent) { throw new Error('remember with --heading requires --parent or heading resolution not yet implemented'); } parentUid = getParentUid(params, context); const action = { action: 'create-block', string: memoryText, location: { 'parent-uid': parentUid, order: params.order ?? 'last' } }; if (params.as) { action.uid = registerPlaceholder(params.as, context); } return [action]; } function translatePage(cmd, context) { // Note: Page creation uses create-page API, not batch actions // We'll handle this specially in the batch command // For now, throw an error to indicate this needs special handling throw new Error('page command requires special handling outside batch actions'); } function translateCodeblock(cmd, context) { const { params } = cmd; const parentUid = getParentUid(params, context); // Format code with triple backticks const codeContent = '```' + params.language + '\n' + params.code + '\n```'; const action = { action: 'create-block', string: codeContent, location: { 'parent-uid': parentUid, order: params.order ?? 'last' } }; if (params.as) { action.uid = registerPlaceholder(params.as, context); } return [action]; } /** * Translate all commands to batch actions */ export function translateAllCommands(commands, context) { const actions = []; const pageCommands = []; for (const cmd of commands) { if (cmd.command === 'page') { // Collect page commands for special handling pageCommands.push(cmd); } else { const cmdActions = translateCommand(cmd, context); actions.push(...cmdActions); } } return { actions, pageCommands }; }