latitude-mcp-server 2.2.5 → 2.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.d.ts CHANGED
@@ -65,6 +65,24 @@ export declare function computeDiff(incoming: Array<{
65
65
  content: string;
66
66
  }>, existing: Document[]): DocumentChange[];
67
67
  export declare function runDocument(path: string, parameters?: Record<string, unknown>, versionUuid?: string): Promise<RunResult>;
68
+ export interface ValidationIssue {
69
+ type: 'error' | 'warning';
70
+ code: string;
71
+ message: string;
72
+ rootCause: string;
73
+ suggestion: string;
74
+ location?: {
75
+ line: number;
76
+ column: number;
77
+ };
78
+ codeFrame?: string;
79
+ }
80
+ /**
81
+ * Pre-validate PromptL content using the official promptl-ai library.
82
+ * Returns detailed, actionable error messages with code frames.
83
+ * EXPORTED for use by tools.ts to pre-validate before pushing.
84
+ */
85
+ export declare function validatePromptLContent(content: string, path: string): Promise<ValidationIssue[]>;
68
86
  /**
69
87
  * Deploy changes to LIVE version using the proper workflow:
70
88
  * 1. Create a draft version
package/dist/api.js CHANGED
@@ -27,6 +27,7 @@ exports.pushChanges = pushChanges;
27
27
  exports.hashContent = hashContent;
28
28
  exports.computeDiff = computeDiff;
29
29
  exports.runDocument = runDocument;
30
+ exports.validatePromptLContent = validatePromptLContent;
30
31
  exports.deployToLive = deployToLive;
31
32
  exports.getPromptNames = getPromptNames;
32
33
  const logger_util_js_1 = require("./utils/logger.util.js");
@@ -443,16 +444,10 @@ const ERROR_SUGGESTIONS = {
443
444
  suggestion: 'Move the <tool-call> tag inside an <assistant> block.',
444
445
  },
445
446
  };
446
- /**
447
- * Get snippet from content around a position
448
- */
449
- function getSnippet(content, startIndex, endIndex) {
450
- const snippet = content.substring(startIndex, Math.min(endIndex, startIndex + 100));
451
- return snippet.split('\n')[0] || snippet;
452
- }
453
447
  /**
454
448
  * Pre-validate PromptL content using the official promptl-ai library.
455
- * Returns detailed, actionable error messages.
449
+ * Returns detailed, actionable error messages with code frames.
450
+ * EXPORTED for use by tools.ts to pre-validate before pushing.
456
451
  */
457
452
  async function validatePromptLContent(content, path) {
458
453
  const issues = [];
@@ -465,9 +460,6 @@ async function validatePromptLContent(content, path) {
465
460
  });
466
461
  // Convert CompileErrors to our ValidationIssue format
467
462
  for (const compileError of result.errors) {
468
- // CompileError has start?.line (Position) and startIndex (number)
469
- const lineNumber = compileError.start?.line;
470
- const snippet = getSnippet(content, compileError.startIndex, compileError.endIndex);
471
463
  // Get human-readable suggestion based on error code
472
464
  const suggestionInfo = ERROR_SUGGESTIONS[compileError.code] || {
473
465
  rootCause: compileError.message,
@@ -475,36 +467,43 @@ async function validatePromptLContent(content, path) {
475
467
  };
476
468
  issues.push({
477
469
  type: 'error',
470
+ code: compileError.code,
478
471
  message: compileError.message,
479
472
  rootCause: suggestionInfo.rootCause,
480
473
  suggestion: suggestionInfo.suggestion,
481
- lineNumber,
482
- snippet,
474
+ location: compileError.start ? {
475
+ line: compileError.start.line,
476
+ column: compileError.start.column,
477
+ } : undefined,
478
+ codeFrame: compileError.frame, // The beautiful formatted code frame!
483
479
  });
484
480
  }
485
481
  }
486
482
  catch (err) {
487
483
  // Handle parse errors (thrown, not accumulated)
488
484
  if (err instanceof promptl_ai_1.CompileError) {
489
- const lineNumber = err.start?.line;
490
- const snippet = getSnippet(content, err.startIndex, err.endIndex);
491
485
  const suggestionInfo = ERROR_SUGGESTIONS[err.code] || {
492
486
  rootCause: err.message,
493
487
  suggestion: 'Fix the syntax error at the indicated location.',
494
488
  };
495
489
  issues.push({
496
490
  type: 'error',
491
+ code: err.code,
497
492
  message: err.message,
498
493
  rootCause: suggestionInfo.rootCause,
499
494
  suggestion: suggestionInfo.suggestion,
500
- lineNumber,
501
- snippet,
495
+ location: err.start ? {
496
+ line: err.start.line,
497
+ column: err.start.column,
498
+ } : undefined,
499
+ codeFrame: err.frame,
502
500
  });
503
501
  }
504
502
  else {
505
503
  // Unknown error - still report it
506
504
  issues.push({
507
505
  type: 'error',
506
+ code: 'unknown-error',
508
507
  message: err instanceof Error ? err.message : 'Unknown validation error',
509
508
  rootCause: 'An unexpected error occurred during validation.',
510
509
  suggestion: 'Check the prompt content for syntax errors.',
@@ -533,10 +532,11 @@ async function identifyFailingDocuments(changes) {
533
532
  failed.push({
534
533
  path: change.path,
535
534
  error: mainError.message,
535
+ code: mainError.code,
536
536
  rootCause: mainError.rootCause,
537
537
  suggestion: mainError.suggestion,
538
- lineNumber: mainError.lineNumber,
539
- snippet: mainError.snippet,
538
+ location: mainError.location,
539
+ codeFrame: mainError.codeFrame,
540
540
  });
541
541
  }
542
542
  }
@@ -601,9 +601,9 @@ async function testSingleDocument(change) {
601
601
  return {
602
602
  path: change.path,
603
603
  error: errorMsg,
604
+ code: 'api-validation-error',
604
605
  rootCause,
605
606
  suggestion,
606
- snippet: change.content?.substring(0, 300),
607
607
  };
608
608
  }
609
609
  }
@@ -731,13 +731,14 @@ async function deployToLive(changes, _versionName) {
731
731
  const errorLines = [];
732
732
  for (const doc of failedDocs) {
733
733
  errorLines.push(`\n## ❌ ${doc.path}`);
734
+ errorLines.push(`**Error Code:** \`${doc.code}\``);
734
735
  errorLines.push(`**Error:** ${doc.error}`);
735
736
  errorLines.push(`**Root Cause:** ${doc.rootCause}`);
736
- if (doc.lineNumber) {
737
- errorLines.push(`**Location:** Line ${doc.lineNumber}`);
737
+ if (doc.location) {
738
+ errorLines.push(`**Location:** Line ${doc.location.line}, Column ${doc.location.column}`);
738
739
  }
739
- if (doc.snippet) {
740
- errorLines.push(`**Snippet:** \`${doc.snippet}\``);
740
+ if (doc.codeFrame) {
741
+ errorLines.push(`**Code Context:**\n\`\`\`\n${doc.codeFrame}\n\`\`\``);
741
742
  }
742
743
  errorLines.push(`**Fix:** ${doc.suggestion}`);
743
744
  }
package/dist/tools.js CHANGED
@@ -106,6 +106,55 @@ function formatAvailablePrompts(names) {
106
106
  const formatted = names.map(n => `\`${n}\``).join(', ');
107
107
  return `\n\n**Available prompts (${names.length}):** ${formatted}`;
108
108
  }
109
+ /**
110
+ * Validate all prompts BEFORE pushing.
111
+ * If ANY prompt fails validation, returns all errors and NOTHING is pushed.
112
+ */
113
+ async function validateAllPrompts(prompts) {
114
+ const errors = [];
115
+ for (const prompt of prompts) {
116
+ const issues = await (0, api_js_1.validatePromptLContent)(prompt.content, prompt.name);
117
+ const errorIssues = issues.filter(i => i.type === 'error');
118
+ if (errorIssues.length > 0) {
119
+ errors.push({ name: prompt.name, issues: errorIssues });
120
+ }
121
+ }
122
+ return {
123
+ valid: errors.length === 0,
124
+ errors,
125
+ };
126
+ }
127
+ /**
128
+ * Format validation errors into a detailed MCP-friendly error message.
129
+ */
130
+ function formatValidationErrors(errors) {
131
+ const lines = [
132
+ `## ❌ Validation Failed - No Changes Made\n`,
133
+ `**${errors.length} prompt(s) have errors.** Fix all errors before pushing.\n`,
134
+ ];
135
+ for (const { name, issues } of errors) {
136
+ for (const issue of issues) {
137
+ lines.push(`### ${name}`);
138
+ lines.push(`**Error Code:** \`${issue.code}\``);
139
+ lines.push(`**Error:** ${issue.message}`);
140
+ lines.push(`**Root Cause:** ${issue.rootCause}`);
141
+ if (issue.location) {
142
+ lines.push(`**Location:** Line ${issue.location.line}, Column ${issue.location.column}`);
143
+ }
144
+ if (issue.codeFrame) {
145
+ lines.push(`**Code Context:**\n\`\`\`\n${issue.codeFrame}\n\`\`\``);
146
+ }
147
+ lines.push(`**Fix:** ${issue.suggestion}`);
148
+ lines.push('');
149
+ }
150
+ }
151
+ lines.push(`---`);
152
+ lines.push(`**Action Required:** Fix the errors above, then retry.`);
153
+ return {
154
+ content: [{ type: 'text', text: lines.join('\n') }],
155
+ isError: true,
156
+ };
157
+ }
109
158
  // ============================================================================
110
159
  // Tool Handlers
111
160
  // ============================================================================
@@ -194,6 +243,15 @@ async function handlePushPrompts(args) {
194
243
  if (prompts.length === 0) {
195
244
  return formatError(new Error('No prompts provided. Use either prompts array or filePaths.'));
196
245
  }
246
+ // PRE-VALIDATE ALL PROMPTS BEFORE PUSHING
247
+ // If ANY prompt fails validation, return errors and push NOTHING
248
+ logger.info(`Validating ${prompts.length} prompt(s) before push...`);
249
+ const validation = await validateAllPrompts(prompts);
250
+ if (!validation.valid) {
251
+ logger.warn(`Validation failed for ${validation.errors.length} prompt(s)`);
252
+ return formatValidationErrors(validation.errors);
253
+ }
254
+ logger.info(`All ${prompts.length} prompt(s) passed validation`);
197
255
  // Get existing prompts for diff computation
198
256
  const existingDocs = await (0, api_js_1.listDocuments)('live');
199
257
  // Compute diff - this determines what needs to be added, modified, or deleted
@@ -279,6 +337,15 @@ async function handleAppendPrompts(args) {
279
337
  if (prompts.length === 0) {
280
338
  return formatError(new Error('No prompts provided. Use either prompts array or filePaths.'));
281
339
  }
340
+ // PRE-VALIDATE ALL PROMPTS BEFORE APPENDING
341
+ // If ANY prompt fails validation, return errors and append NOTHING
342
+ logger.info(`Validating ${prompts.length} prompt(s) before append...`);
343
+ const validation = await validateAllPrompts(prompts);
344
+ if (!validation.valid) {
345
+ logger.warn(`Validation failed for ${validation.errors.length} prompt(s)`);
346
+ return formatValidationErrors(validation.errors);
347
+ }
348
+ logger.info(`All ${prompts.length} prompt(s) passed validation`);
282
349
  // Get existing prompts
283
350
  const existingDocs = await (0, api_js_1.listDocuments)('live');
284
351
  const existingMap = new Map(existingDocs.map((d) => [d.path, d]));
@@ -454,6 +521,15 @@ async function handleReplacePrompt(args) {
454
521
  if (!content) {
455
522
  return formatError(new Error('Prompt content is required. Provide either `content` or `filePath`.'));
456
523
  }
524
+ // PRE-VALIDATE PROMPT BEFORE REPLACING
525
+ // If validation fails, return error and replace NOTHING
526
+ logger.info(`Validating prompt "${name}" before replace...`);
527
+ const validation = await validateAllPrompts([{ name, content }]);
528
+ if (!validation.valid) {
529
+ logger.warn(`Validation failed for prompt "${name}"`);
530
+ return formatValidationErrors(validation.errors);
531
+ }
532
+ logger.info(`Prompt "${name}" passed validation`);
457
533
  // Check if prompt exists
458
534
  const existingDocs = await (0, api_js_1.listDocuments)('live');
459
535
  const exists = existingDocs.some((d) => d.path === name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latitude-mcp-server",
3
- "version": "2.2.5",
3
+ "version": "2.2.7",
4
4
  "description": "Simplified MCP server for Latitude.so prompt management - 8 focused tools for push, pull, run, and manage prompts",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",