latitude-mcp-server 2.1.7 → 2.2.2

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
@@ -16,24 +16,63 @@ export declare class LatitudeApiError extends Error {
16
16
  readonly errorCode: string;
17
17
  readonly statusCode: number;
18
18
  readonly details?: Record<string, unknown>;
19
- constructor(error: LatitudeError, statusCode: number);
19
+ readonly rawResponse?: string;
20
+ constructor(error: LatitudeError, statusCode: number, rawResponse?: string);
21
+ /**
22
+ * Extract detailed error messages from nested error structures
23
+ */
24
+ getDetailedErrors(): string[];
20
25
  toMarkdown(): string;
26
+ /**
27
+ * Get a concise error message suitable for per-prompt error tracking
28
+ */
29
+ getConciseMessage(): string;
21
30
  }
22
31
  /**
23
32
  * Get project ID from config
24
33
  */
25
34
  export declare function getProjectId(): string;
26
35
  export declare function listVersions(): Promise<Version[]>;
36
+ export declare function getVersion(versionUuid: string): Promise<Version>;
27
37
  export declare function createVersion(name: string): Promise<Version>;
28
38
  export declare function publishVersion(versionUuid: string, title?: string): Promise<Version>;
29
39
  export declare function listDocuments(versionUuid?: string): Promise<Document[]>;
30
40
  export declare function getDocument(path: string, versionUuid?: string): Promise<Document>;
31
41
  export declare function createOrUpdateDocument(versionUuid: string, path: string, content: string, force?: boolean): Promise<Document>;
32
42
  export declare function deleteDocument(versionUuid: string, path: string): Promise<void>;
43
+ /**
44
+ * Push response from the API
45
+ */
46
+ interface PushResponse {
47
+ versionUuid: string;
48
+ documentsProcessed: number;
49
+ }
50
+ /**
51
+ * Push changes to a version in a single batch
52
+ * This is the CLI-style push that sends all changes at once
53
+ */
54
+ export declare function pushChanges(versionUuid: string, changes: DocumentChange[]): Promise<PushResponse>;
55
+ /**
56
+ * Compute hash of content for diff comparison
57
+ */
58
+ export declare function hashContent(content: string): string;
59
+ /**
60
+ * Compute diff between incoming prompts and existing prompts
61
+ * Returns only the changes that need to be made
62
+ */
63
+ export declare function computeDiff(incoming: Array<{
64
+ path: string;
65
+ content: string;
66
+ }>, existing: Document[]): DocumentChange[];
33
67
  export declare function runDocument(path: string, parameters?: Record<string, unknown>, versionUuid?: string): Promise<RunResult>;
34
68
  /**
35
- * Deploy changes directly to LIVE version using force mode
36
- * This bypasses the draft->publish workflow which requires provider validation
69
+ * Deploy changes to LIVE version using the proper workflow:
70
+ * 1. Create a draft version
71
+ * 2. Push all changes to the draft (batch)
72
+ * 3. Publish the draft to make it LIVE
73
+ *
74
+ * This is the same workflow the CLI uses, ensuring proper validation.
37
75
  */
38
76
  export declare function deployToLive(changes: DocumentChange[], _versionName?: string): Promise<DeployResult>;
39
77
  export declare function getPromptNames(): Promise<string[]>;
78
+ export {};
package/dist/api.js CHANGED
@@ -16,12 +16,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
16
16
  exports.LatitudeApiError = void 0;
17
17
  exports.getProjectId = getProjectId;
18
18
  exports.listVersions = listVersions;
19
+ exports.getVersion = getVersion;
19
20
  exports.createVersion = createVersion;
20
21
  exports.publishVersion = publishVersion;
21
22
  exports.listDocuments = listDocuments;
22
23
  exports.getDocument = getDocument;
23
24
  exports.createOrUpdateDocument = createOrUpdateDocument;
24
25
  exports.deleteDocument = deleteDocument;
26
+ exports.pushChanges = pushChanges;
27
+ exports.hashContent = hashContent;
28
+ exports.computeDiff = computeDiff;
25
29
  exports.runDocument = runDocument;
26
30
  exports.deployToLive = deployToLive;
27
31
  exports.getPromptNames = getPromptNames;
@@ -57,6 +61,13 @@ async function request(endpoint, options = {}) {
57
61
  const timeoutId = setTimeout(() => controller.abort(), options.timeout || API_TIMEOUT_MS);
58
62
  logger.debug(`API ${method} ${endpoint}`);
59
63
  try {
64
+ // Add __internal: { source: 'api' } to all POST requests (required by Latitude API)
65
+ const body = options.body && method === 'POST'
66
+ ? { ...options.body, __internal: { source: 'api' } }
67
+ : options.body;
68
+ if (body) {
69
+ logger.debug(`Request body: ${JSON.stringify(body, null, 2)}`);
70
+ }
60
71
  const response = await fetch(url, {
61
72
  method,
62
73
  headers: {
@@ -64,7 +75,7 @@ async function request(endpoint, options = {}) {
64
75
  Accept: 'application/json',
65
76
  Authorization: `Bearer ${apiKey}`,
66
77
  },
67
- body: options.body ? JSON.stringify(options.body) : undefined,
78
+ body: body ? JSON.stringify(body) : undefined,
68
79
  signal: controller.signal,
69
80
  });
70
81
  clearTimeout(timeoutId);
@@ -72,7 +83,17 @@ async function request(endpoint, options = {}) {
72
83
  const errorText = await response.text();
73
84
  let errorData;
74
85
  try {
75
- errorData = JSON.parse(errorText);
86
+ const parsed = JSON.parse(errorText);
87
+ // Capture the ENTIRE parsed response as details
88
+ // This ensures we never lose any error information from the API
89
+ const { name, errorCode, code, message, ...rest } = parsed;
90
+ errorData = {
91
+ name: name || 'APIError',
92
+ errorCode: errorCode || code || `HTTP_${response.status}`,
93
+ message: message || `HTTP ${response.status}`,
94
+ // Store ALL remaining fields as details
95
+ details: Object.keys(rest).length > 0 ? rest : undefined,
96
+ };
76
97
  }
77
98
  catch {
78
99
  errorData = {
@@ -81,7 +102,8 @@ async function request(endpoint, options = {}) {
81
102
  message: errorText || `HTTP ${response.status}`,
82
103
  };
83
104
  }
84
- throw new LatitudeApiError(errorData, response.status);
105
+ logger.debug(`API error response: ${errorText}`);
106
+ throw new LatitudeApiError(errorData, response.status, errorText);
85
107
  }
86
108
  // Handle empty responses
87
109
  const contentLength = response.headers.get('content-length');
@@ -113,12 +135,81 @@ async function request(endpoint, options = {}) {
113
135
  // Error Class
114
136
  // ============================================================================
115
137
  class LatitudeApiError extends Error {
116
- constructor(error, statusCode) {
138
+ constructor(error, statusCode, rawResponse) {
117
139
  super(error.message);
118
140
  this.name = error.name;
119
141
  this.errorCode = error.errorCode;
120
142
  this.statusCode = statusCode;
121
143
  this.details = error.details;
144
+ this.rawResponse = rawResponse;
145
+ }
146
+ /**
147
+ * Extract detailed error messages from nested error structures
148
+ */
149
+ getDetailedErrors() {
150
+ const errors = [];
151
+ if (!this.details)
152
+ return errors;
153
+ // Handle errors array in details
154
+ if (Array.isArray(this.details.errors)) {
155
+ for (const err of this.details.errors) {
156
+ if (typeof err === 'string') {
157
+ errors.push(err);
158
+ }
159
+ else if (err && typeof err === 'object') {
160
+ const errObj = err;
161
+ const msg = errObj.message || errObj.error || errObj.detail || JSON.stringify(err);
162
+ const path = errObj.path || errObj.document || errObj.name || '';
163
+ errors.push(path ? `${path}: ${msg}` : String(msg));
164
+ }
165
+ }
166
+ }
167
+ // Handle documents with errors
168
+ if (this.details.documents && typeof this.details.documents === 'object') {
169
+ const docs = this.details.documents;
170
+ for (const [docPath, docInfo] of Object.entries(docs)) {
171
+ if (docInfo && typeof docInfo === 'object') {
172
+ const info = docInfo;
173
+ if (info.error || info.errors) {
174
+ const docErrors = info.errors || [info.error];
175
+ for (const e of Array.isArray(docErrors) ? docErrors : [docErrors]) {
176
+ errors.push(`${docPath}: ${typeof e === 'string' ? e : JSON.stringify(e)}`);
177
+ }
178
+ }
179
+ }
180
+ }
181
+ }
182
+ // Handle validation errors object
183
+ if (this.details.validationErrors && typeof this.details.validationErrors === 'object') {
184
+ const valErrors = this.details.validationErrors;
185
+ for (const [field, fieldErrors] of Object.entries(valErrors)) {
186
+ if (Array.isArray(fieldErrors)) {
187
+ for (const fe of fieldErrors) {
188
+ errors.push(`${field}: ${typeof fe === 'string' ? fe : JSON.stringify(fe)}`);
189
+ }
190
+ }
191
+ else {
192
+ errors.push(`${field}: ${typeof fieldErrors === 'string' ? fieldErrors : JSON.stringify(fieldErrors)}`);
193
+ }
194
+ }
195
+ }
196
+ // Handle cause field
197
+ if (this.details.cause) {
198
+ const cause = this.details.cause;
199
+ if (typeof cause === 'string') {
200
+ errors.push(cause);
201
+ }
202
+ else if (typeof cause === 'object') {
203
+ const causeObj = cause;
204
+ if (causeObj.message) {
205
+ errors.push(String(causeObj.message));
206
+ }
207
+ else {
208
+ errors.push(JSON.stringify(cause));
209
+ }
210
+ }
211
+ }
212
+ return errors;
122
213
  }
123
214
  toMarkdown() {
124
215
  let md = `## ❌ Error: ${this.name}\n\n`;
@@ -127,11 +218,45 @@ class LatitudeApiError extends Error {
127
218
  if (this.statusCode) {
128
219
  md += `\n**HTTP Status:** ${this.statusCode}\n`;
129
220
  }
221
+ // Try to extract detailed errors first
222
+ const detailedErrors = this.getDetailedErrors();
223
+ if (detailedErrors.length > 0) {
224
+ md += `\n**Detailed Errors (${detailedErrors.length}):**\n`;
225
+ for (const err of detailedErrors) {
226
+ md += `- ${err}\n`;
227
+ }
228
+ }
229
+ // Always show details if present (structured data from API)
130
230
  if (this.details && Object.keys(this.details).length > 0) {
131
- md += `\n**Details:**\n\`\`\`json\n${JSON.stringify(this.details, null, 2)}\n\`\`\`\n`;
231
+ md += `\n**API Response Details:**\n\`\`\`json\n${JSON.stringify(this.details, null, 2)}\n\`\`\`\n`;
232
+ }
233
+ // Always show raw response if available (for debugging)
234
+ if (this.rawResponse && this.rawResponse !== JSON.stringify(this.details)) {
235
+ md += `\n**Raw API Response:**\n\`\`\`json\n${this.rawResponse}\n\`\`\`\n`;
132
236
  }
133
237
  return md;
134
238
  }
239
+ /**
240
+ * Get a concise error message suitable for per-prompt error tracking
241
+ */
242
+ getConciseMessage() {
243
+ const detailed = this.getDetailedErrors();
244
+ if (detailed.length > 0) {
245
+ return detailed.join('; ');
246
+ }
247
+ // If no detailed errors, include details summary if available
248
+ if (this.details && Object.keys(this.details).length > 0) {
249
+ return `${this.message} | Details: ${JSON.stringify(this.details)}`;
250
+ }
251
+ // Last resort: include raw response snippet
252
+ if (this.rawResponse) {
253
+ const snippet = this.rawResponse.length > 200
254
+ ? this.rawResponse.substring(0, 200) + '...'
255
+ : this.rawResponse;
256
+ return `${this.message} | Raw: ${snippet}`;
257
+ }
258
+ return this.message;
259
+ }
135
260
  }
136
261
  exports.LatitudeApiError = LatitudeApiError;
137
262
  // ============================================================================
@@ -147,6 +272,10 @@ async function listVersions() {
147
272
  const projectId = getProjectId();
148
273
  return request(`/projects/${projectId}/versions`);
149
274
  }
275
+ async function getVersion(versionUuid) {
276
+ const projectId = getProjectId();
277
+ return request(`/projects/${projectId}/versions/${versionUuid}`);
278
+ }
150
279
  async function createVersion(name) {
151
280
  const projectId = getProjectId();
152
281
  return request(`/projects/${projectId}/versions`, {
@@ -184,6 +313,78 @@ async function deleteDocument(versionUuid, path) {
184
313
  const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
185
314
  await request(`/projects/${projectId}/versions/${versionUuid}/documents/${normalizedPath}`, { method: 'DELETE' });
186
315
  }
316
+ /**
317
+ * Push changes to a version in a single batch
318
+ * This is the CLI-style push that sends all changes at once
319
+ */
320
+ async function pushChanges(versionUuid, changes) {
321
+ const projectId = getProjectId();
322
+ // Format changes for the API
323
+ const apiChanges = changes.map((c) => ({
324
+ path: c.path,
325
+ content: c.content || '',
326
+ status: c.status,
327
+ }));
328
+ logger.info(`Pushing ${changes.length} change(s) to version ${versionUuid}`);
329
+ return request(`/projects/${projectId}/versions/${versionUuid}/push`, {
330
+ method: 'POST',
331
+ body: { changes: apiChanges },
332
+ });
333
+ }
334
+ /**
335
+ * Compute hash of content for diff comparison
336
+ */
337
+ function hashContent(content) {
338
+ // Simple hash - in production you might want crypto
339
+ let hash = 0;
340
+ for (let i = 0; i < content.length; i++) {
341
+ const char = content.charCodeAt(i);
342
+ hash = ((hash << 5) - hash) + char;
343
+ hash = hash & hash; // Convert to 32bit integer
344
+ }
345
+ return hash.toString(16);
346
+ }
347
+ /**
348
+ * Compute diff between incoming prompts and existing prompts
349
+ * Returns only the changes that need to be made
350
+ */
351
+ function computeDiff(incoming, existing) {
352
+ const changes = [];
353
+ const existingMap = new Map(existing.map((d) => [d.path, d]));
354
+ const incomingPaths = new Set(incoming.map((p) => p.path));
355
+ // Check each incoming prompt
356
+ for (const prompt of incoming) {
357
+ const existingDoc = existingMap.get(prompt.path);
358
+ if (!existingDoc) {
359
+ // New prompt
360
+ changes.push({
361
+ path: prompt.path,
362
+ content: prompt.content,
363
+ status: 'added',
364
+ });
365
+ }
366
+ else if (existingDoc.content !== prompt.content) {
367
+ // Modified prompt
368
+ changes.push({
369
+ path: prompt.path,
370
+ content: prompt.content,
371
+ status: 'modified',
372
+ });
373
+ }
374
+ // If content is same, no change needed (don't include in changes)
375
+ }
376
+ // Check for deleted prompts (exist remotely but not in incoming)
377
+ for (const path of existingMap.keys()) {
378
+ if (!incomingPaths.has(path)) {
379
+ changes.push({
380
+ path,
381
+ content: '',
382
+ status: 'deleted',
383
+ });
384
+ }
385
+ }
386
+ return changes;
387
+ }
187
388
  async function runDocument(path, parameters, versionUuid = 'live') {
188
389
  const projectId = getProjectId();
189
390
  return request(`/projects/${projectId}/versions/${versionUuid}/documents/run`, {
@@ -197,45 +398,74 @@ async function runDocument(path, parameters, versionUuid = 'live') {
197
398
  });
198
399
  }
199
400
  /**
200
- * Deploy changes directly to LIVE version using force mode
201
- * This bypasses the draft->publish workflow which requires provider validation
401
+ * Create a synthetic Version object for no-op deploys.
402
+ * Returns a fully populated Version with all required fields.
403
+ */
404
+ function createNoOpVersion() {
405
+ const now = new Date().toISOString();
406
+ return {
407
+ id: 0,
408
+ uuid: 'live',
409
+ projectId: 0,
410
+ message: 'No changes to deploy',
411
+ createdAt: now,
412
+ updatedAt: now,
413
+ status: 'live',
414
+ };
415
+ }
416
+ /**
417
+ * Deploy changes to LIVE version using the proper workflow:
418
+ * 1. Create a draft version
419
+ * 2. Push all changes to the draft (batch)
420
+ * 3. Publish the draft to make it LIVE
421
+ *
422
+ * This is the same workflow the CLI uses, ensuring proper validation.
202
423
  */
203
424
  async function deployToLive(changes, _versionName) {
204
- const added = [];
205
- const modified = [];
206
- const deleted = [];
207
- // Process each change directly to LIVE with force=true
208
- for (const change of changes) {
209
- logger.info(`Deploying ${change.status}: ${change.path}`);
210
- if (change.status === 'deleted') {
211
- try {
212
- await deleteDocument('live', change.path);
213
- deleted.push(change.path);
214
- logger.debug(`Deleted: ${change.path}`);
215
- }
216
- catch (error) {
217
- // Document might not exist, log and continue
218
- logger.warn(`Failed to delete ${change.path}:`, error);
219
- }
220
- }
221
- else {
222
- // Use force=true to push directly to LIVE
223
- await createOrUpdateDocument('live', change.path, change.content, true);
224
- if (change.status === 'added') {
225
- added.push(change.path);
226
- }
227
- else {
228
- modified.push(change.path);
229
- }
230
- logger.debug(`${change.status}: ${change.path}`);
231
- }
425
+ if (changes.length === 0) {
426
+ logger.info('No changes to deploy');
427
+ return {
428
+ version: createNoOpVersion(),
429
+ documentsProcessed: 0,
430
+ added: [],
431
+ modified: [],
432
+ deleted: [],
433
+ };
434
+ }
435
+ // Filter out unchanged items (they shouldn't be sent to API)
436
+ const actualChanges = changes.filter(c => c.status !== 'unchanged');
437
+ if (actualChanges.length === 0) {
438
+ logger.info('All prompts are unchanged, nothing to deploy');
439
+ return {
440
+ version: createNoOpVersion(),
441
+ documentsProcessed: 0,
442
+ added: [],
443
+ modified: [],
444
+ deleted: [],
445
+ };
232
446
  }
233
- // Get current LIVE version info
234
- const docs = await listDocuments('live');
235
- const liveVersionUuid = docs.length > 0 ? docs[0].versionUuid : 'live';
447
+ // Categorize changes for logging and return value
448
+ const added = actualChanges.filter((c) => c.status === 'added').map((c) => c.path);
449
+ const modified = actualChanges.filter((c) => c.status === 'modified').map((c) => c.path);
450
+ const deleted = actualChanges.filter((c) => c.status === 'deleted').map((c) => c.path);
451
+ logger.info(`Deploying to LIVE: ${added.length} added, ${modified.length} modified, ${deleted.length} deleted`);
452
+ // Step 1: Create a new draft version
453
+ const draftName = `MCP deploy ${new Date().toISOString()}`;
454
+ logger.info(`Creating draft version: ${draftName}`);
455
+ const draft = await createVersion(draftName);
456
+ logger.info(`Draft created: ${draft.uuid}`);
457
+ // Step 2: Push all changes to the draft in ONE batch
458
+ logger.info(`Pushing ${actualChanges.length} change(s) to draft...`);
459
+ logger.debug(`Changes payload: ${JSON.stringify(actualChanges, null, 2)}`);
460
+ const pushResult = await pushChanges(draft.uuid, actualChanges);
461
+ logger.info(`Push complete: ${pushResult.documentsProcessed} documents processed`);
462
+ // Step 3: Publish the draft to make it LIVE
463
+ logger.info(`Publishing draft ${draft.uuid} to LIVE...`);
464
+ const published = await publishVersion(draft.uuid, draftName);
465
+ logger.info(`Published successfully! Version is now LIVE: ${published.uuid}`);
236
466
  return {
237
- version: { uuid: liveVersionUuid },
238
- documentsProcessed: changes.length,
467
+ version: published,
468
+ documentsProcessed: pushResult.documentsProcessed,
239
469
  added,
240
470
  modified,
241
471
  deleted,
package/dist/tools.js CHANGED
@@ -194,53 +194,52 @@ async function handlePushPrompts(args) {
194
194
  if (prompts.length === 0) {
195
195
  return formatError(new Error('No prompts provided. Use either prompts array or filePaths.'));
196
196
  }
197
- // Get existing prompts to delete
197
+ // Get existing prompts for diff computation
198
198
  const existingDocs = await (0, api_js_1.listDocuments)('live');
199
- const existingNames = existingDocs.map((d) => d.path);
200
- // Step 1: Delete all existing prompts in one batch (deletions are small)
201
- if (existingNames.length > 0) {
202
- const deleteChanges = existingNames.map((name) => ({
203
- path: name,
204
- content: '',
205
- status: 'deleted',
206
- }));
207
- await (0, api_js_1.deployToLive)(deleteChanges, 'push (delete existing)');
199
+ // Compute diff - this determines what needs to be added, modified, or deleted
200
+ const incoming = prompts.map(p => ({ path: p.name, content: p.content }));
201
+ const changes = (0, api_js_1.computeDiff)(incoming, existingDocs);
202
+ // Summarize changes
203
+ const added = changes.filter((c) => c.status === 'added');
204
+ const modified = changes.filter((c) => c.status === 'modified');
205
+ const deleted = changes.filter((c) => c.status === 'deleted');
206
+ if (changes.length === 0) {
207
+ const newNames = await forceRefreshAndGetNames();
208
+ return formatSuccess('No Changes Needed', `All ${prompts.length} prompt(s) are already up to date.\n\n` +
209
+ `**Current LIVE prompts (${newNames.length}):** ${newNames.map(n => `\`${n}\``).join(', ')}`);
208
210
  }
209
- // Step 2: Add each new prompt INDIVIDUALLY to avoid payload size limits
210
- const added = [];
211
- const errors = [];
212
- for (const prompt of prompts) {
213
- try {
214
- const changes = [
215
- {
216
- path: prompt.name,
217
- content: prompt.content,
218
- status: 'added',
219
- },
220
- ];
221
- await (0, api_js_1.deployToLive)(changes, `push ${prompt.name}`);
222
- added.push(prompt.name);
211
+ // Push all changes in one batch
212
+ try {
213
+ const result = await (0, api_js_1.deployToLive)(changes, 'push');
214
+ // Force refresh cache after mutations
215
+ const newNames = await forceRefreshAndGetNames();
216
+ let content = `**Summary:**\n`;
217
+ content += `- Added: ${added.length}\n`;
218
+ content += `- Modified: ${modified.length}\n`;
219
+ content += `- Deleted: ${deleted.length}\n`;
220
+ content += `- Documents processed: ${result.documentsProcessed}\n\n`;
221
+ if (added.length > 0) {
222
+ content += `### Added\n${added.map((c) => `- \`${c.path}\``).join('\n')}\n\n`;
223
223
  }
224
- catch (error) {
225
- const msg = error instanceof Error ? error.message : String(error);
226
- errors.push(`${prompt.name}: ${msg}`);
224
+ if (modified.length > 0) {
225
+ content += `### Modified\n${modified.map((c) => `- \`${c.path}\``).join('\n')}\n\n`;
227
226
  }
227
+ if (deleted.length > 0) {
228
+ content += `### Deleted\n${deleted.map((c) => `- \`${c.path}\``).join('\n')}\n\n`;
229
+ }
230
+ content += `---\n**Current LIVE prompts (${newNames.length}):** ${newNames.map(n => `\`${n}\``).join(', ')}`;
231
+ return formatSuccess('Prompts Pushed to LIVE', content);
228
232
  }
229
- // Force refresh cache after mutations
230
- const newNames = await forceRefreshAndGetNames();
231
- let content = `**Deleted:** ${existingNames.length} prompt(s)\n`;
232
- content += `**Added:** ${added.length} prompt(s)\n`;
233
- if (errors.length > 0) {
234
- content += `**Errors:** ${errors.length}\n`;
235
- content += errors.map((e) => ` - ${e}`).join('\n') + '\n';
236
- }
237
- content += `\n### Deployed Prompts\n\n`;
238
- content += added.map((n) => `- \`${n}\``).join('\n');
239
- content += `\n\n---\n**Current LIVE prompts (${newNames.length}):** ${newNames.map(n => `\`${n}\``).join(', ')}`;
240
- if (added.length === 0 && errors.length > 0) {
241
- return formatError(new Error(`All ${errors.length} prompt(s) failed:\n${errors.join('\n')}`));
233
+ catch (error) {
234
+ // Detailed error from API
235
+ if (error instanceof api_js_1.LatitudeApiError) {
236
+ return {
237
+ content: [{ type: 'text', text: error.toMarkdown() }],
238
+ isError: true,
239
+ };
240
+ }
241
+ throw error;
242
242
  }
243
- return formatSuccess('Prompts Pushed to LIVE', content);
244
243
  }
245
244
  catch (error) {
246
245
  return formatError(error);
@@ -282,64 +281,85 @@ async function handleAppendPrompts(args) {
282
281
  }
283
282
  // Get existing prompts
284
283
  const existingDocs = await (0, api_js_1.listDocuments)('live');
285
- const existingNames = new Set(existingDocs.map((d) => d.path));
286
- // Track results
287
- const added = [];
288
- const updated = [];
284
+ const existingMap = new Map(existingDocs.map((d) => [d.path, d]));
285
+ // Build changes - append does NOT delete existing prompts
286
+ const changes = [];
289
287
  const skipped = [];
290
- const errors = [];
291
- // Process each prompt INDIVIDUALLY to avoid payload size limits
292
288
  for (const prompt of prompts) {
293
- const exists = existingNames.has(prompt.name);
294
- if (exists && !args.overwrite) {
295
- skipped.push(prompt.name);
296
- continue;
297
- }
298
- try {
299
- const changes = [
300
- {
301
- path: prompt.name,
302
- content: prompt.content,
303
- status: exists ? 'modified' : 'added',
304
- },
305
- ];
306
- await (0, api_js_1.deployToLive)(changes, `append ${prompt.name}`);
307
- if (exists) {
308
- updated.push(prompt.name);
289
+ const existingDoc = existingMap.get(prompt.name);
290
+ if (existingDoc) {
291
+ if (args.overwrite) {
292
+ // Only include if content is different
293
+ if (existingDoc.content !== prompt.content) {
294
+ changes.push({
295
+ path: prompt.name,
296
+ content: prompt.content,
297
+ status: 'modified',
298
+ });
299
+ }
300
+ // If same content, skip silently (unchanged)
309
301
  }
310
302
  else {
311
- added.push(prompt.name);
303
+ skipped.push(prompt.name);
312
304
  }
313
305
  }
314
- catch (error) {
315
- const msg = error instanceof Error ? error.message : String(error);
316
- errors.push(`${prompt.name}: ${msg}`);
306
+ else {
307
+ // New prompt
308
+ changes.push({
309
+ path: prompt.name,
310
+ content: prompt.content,
311
+ status: 'added',
312
+ });
317
313
  }
318
314
  }
319
- // Force refresh cache after mutations
320
- const newNames = await forceRefreshAndGetNames();
321
- let content = '';
322
- if (added.length > 0) {
323
- content += `**Added:** ${added.length}\n`;
324
- content += added.map((n) => ` - \`${n}\``).join('\n') + '\n\n';
325
- }
326
- if (updated.length > 0) {
327
- content += `**Updated:** ${updated.length}\n`;
328
- content += updated.map((n) => ` - \`${n}\``).join('\n') + '\n\n';
315
+ // Summarize
316
+ const added = changes.filter((c) => c.status === 'added');
317
+ const modified = changes.filter((c) => c.status === 'modified');
318
+ if (changes.length === 0 && skipped.length === 0) {
319
+ const newNames = await forceRefreshAndGetNames();
320
+ return formatSuccess('No Changes Needed', `All ${prompts.length} prompt(s) are already up to date.\n\n` +
321
+ `**Current LIVE prompts (${newNames.length}):** ${newNames.map(n => `\`${n}\``).join(', ')}`);
329
322
  }
330
- if (skipped.length > 0) {
331
- content += `**Skipped:** ${skipped.length} (already exist)\n`;
323
+ if (changes.length === 0) {
324
+ const newNames = await forceRefreshAndGetNames();
325
+ let content = `**Skipped:** ${skipped.length} (already exist, use overwrite=true to update)\n`;
326
+ content += `\n---\n**Current LIVE prompts (${newNames.length}):** ${newNames.map(n => `\`${n}\``).join(', ')}`;
327
+ return formatSuccess('No Changes Made', content);
332
328
  }
333
- if (errors.length > 0) {
334
- content += `\n**Errors:** ${errors.length}\n`;
335
- content += errors.map((e) => ` - ${e}`).join('\n') + '\n';
329
+ // Push all changes in one batch
330
+ try {
331
+ const result = await (0, api_js_1.deployToLive)(changes, 'append');
332
+ // Force refresh cache after mutations
333
+ const newNames = await forceRefreshAndGetNames();
334
+ let content = `**Summary:**\n`;
335
+ content += `- Added: ${added.length}\n`;
336
+ content += `- Updated: ${modified.length}\n`;
337
+ if (skipped.length > 0) {
338
+ content += `- Skipped: ${skipped.length} (use overwrite=true)\n`;
339
+ }
340
+ content += `- Documents processed: ${result.documentsProcessed}\n\n`;
341
+ if (added.length > 0) {
342
+ content += `### Added\n${added.map((c) => `- \`${c.path}\``).join('\n')}\n\n`;
343
+ }
344
+ if (modified.length > 0) {
345
+ content += `### Updated\n${modified.map((c) => `- \`${c.path}\``).join('\n')}\n\n`;
346
+ }
347
+ if (skipped.length > 0) {
348
+ content += `### Skipped (already exist)\n${skipped.map(n => `- \`${n}\``).join('\n')}\n\n`;
349
+ }
350
+ content += `---\n**Current LIVE prompts (${newNames.length}):** ${newNames.map(n => `\`${n}\``).join(', ')}`;
351
+ return formatSuccess('Prompts Appended to LIVE', content);
336
352
  }
337
- content += `\n---\n**Current LIVE prompts (${newNames.length}):** ${newNames.map(n => `\`${n}\``).join(', ')}`;
338
- const totalProcessed = added.length + updated.length;
339
- if (totalProcessed === 0 && errors.length > 0) {
340
- return formatError(new Error(`All ${errors.length} prompt(s) failed:\n${errors.join('\n')}`));
353
+ catch (error) {
354
+ // Detailed error from API
355
+ if (error instanceof api_js_1.LatitudeApiError) {
356
+ return {
357
+ content: [{ type: 'text', text: error.toMarkdown() }],
358
+ isError: true,
359
+ };
360
+ }
361
+ throw error;
341
362
  }
342
- return formatSuccess('Prompts Appended to LIVE', content);
343
363
  }
344
364
  catch (error) {
345
365
  return formatError(error);
package/dist/types.d.ts CHANGED
@@ -33,7 +33,7 @@ export interface Document {
33
33
  export interface DocumentChange {
34
34
  path: string;
35
35
  content: string;
36
- status: 'added' | 'modified' | 'deleted';
36
+ status: 'added' | 'modified' | 'deleted' | 'unchanged';
37
37
  }
38
38
  export interface DeployResult {
39
39
  version: Version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latitude-mcp-server",
3
- "version": "2.1.7",
3
+ "version": "2.2.2",
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",