UNPKG

@wonderwhy-er/desktop-commander

Version:

MCP server for terminal operations and file editing

104 lines (103 loc) 3.92 kB
import { escapeHtml } from './components/highlighting.js'; import { stripReadStatusLine } from './document-workspace.js'; function isObjectRecord(value) { return typeof value === 'object' && value !== null; } export function getFileExtensionForAnalytics(filePath) { const normalizedPath = filePath.trim().replace(/\\/g, '/'); const fileName = normalizedPath.split('/').pop() ?? normalizedPath; const dotIndex = fileName.lastIndexOf('.'); if (dotIndex <= 0 || dotIndex === fileName.length - 1) { return 'none'; } return fileName.slice(dotIndex + 1).toLowerCase(); } export function isPreviewStructuredContent(value) { if (!isObjectRecord(value)) { return false; } return (typeof value.fileName === 'string' && typeof value.filePath === 'string' && typeof value.fileType === 'string'); } export function buildRenderPayload(meta, text) { return { ...meta, content: text }; } export function extractToolText(value) { if (!isObjectRecord(value)) { return undefined; } const content = value.content; if (!Array.isArray(content)) { return undefined; } for (const item of content) { if (!isObjectRecord(item)) { continue; } if (item.type === 'text' && typeof item.text === 'string' && item.text.trim().length > 0) { return item.text; } } return undefined; } function extractStructuredContentText(value) { if (!isObjectRecord(value)) { return undefined; } return typeof value.content === 'string' ? value.content : undefined; } export function extractRenderPayload(value) { if (!isObjectRecord(value)) { return undefined; } const meta = isPreviewStructuredContent(value.structuredContent) ? value.structuredContent : isPreviewStructuredContent(value) ? value : null; if (!meta) return undefined; const text = extractStructuredContentText(value.structuredContent) ?? extractToolText(value) ?? extractToolText(value.structuredContent) ?? ''; return buildRenderPayload(meta, text); } export function assertSuccessfulEditBlockResult(result) { if (!isObjectRecord(result)) { throw new Error('edit_block did not return a valid result.'); } if (result.isError === true) { const message = extractToolText(result) ?? ''; throw new Error(message || 'edit_block failed.'); } // edit_block uses soft-failure returns (no isError flag) for cases the LLM // is meant to recover from — "Search content not found", "Expected N // occurrences but found M", fuzzy-match-too-close-to-ignore, etc. These // look like success to a naive client. A real success always carries // structuredContent (see src/tools/edit.ts — the write path attaches // fileName/filePath/fileType); absence means the edit did not land. // Throwing here routes soft failures through saveDocument's catch, which // reloads disk, preserves the user's draft, and surfaces the server's // message to the user. if (!isObjectRecord(result.structuredContent)) { const message = extractToolText(result) ?? ''; throw new Error(message || 'edit_block did not confirm success.'); } } export function isLikelyUrl(filePath) { return /^https?:\/\//i.test(filePath); } export function buildBreadcrumb(filePath) { const normalized = filePath.replace(/\\/g, '/'); const parts = normalized.split('/').filter(Boolean); return parts.map((part) => escapeHtml(part)).join(' <span class="breadcrumb-sep">›</span> '); } export function countContentLines(content) { const cleaned = stripReadStatusLine(content); if (cleaned === '') return 0; const lines = cleaned.split('\n'); return lines[lines.length - 1] === '' ? lines.length - 1 : lines.length; }