latitude-mcp-server 2.1.6 → 2.2.1

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,8 +16,17 @@ 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
@@ -28,12 +37,41 @@ export declare function createVersion(name: string): Promise<Version>;
28
37
  export declare function publishVersion(versionUuid: string, title?: string): Promise<Version>;
29
38
  export declare function listDocuments(versionUuid?: string): Promise<Document[]>;
30
39
  export declare function getDocument(path: string, versionUuid?: string): Promise<Document>;
31
- export declare function createOrUpdateDocument(versionUuid: string, path: string, content: string): Promise<Document>;
40
+ export declare function createOrUpdateDocument(versionUuid: string, path: string, content: string, force?: boolean): Promise<Document>;
32
41
  export declare function deleteDocument(versionUuid: string, path: string): Promise<void>;
42
+ /**
43
+ * Push response from the API
44
+ */
45
+ interface PushResponse {
46
+ versionUuid: string;
47
+ documentsProcessed: number;
48
+ }
49
+ /**
50
+ * Push changes to a version in a single batch
51
+ * This is the CLI-style push that sends all changes at once
52
+ */
53
+ export declare function pushChanges(versionUuid: string, changes: DocumentChange[]): Promise<PushResponse>;
54
+ /**
55
+ * Compute hash of content for diff comparison
56
+ */
57
+ export declare function hashContent(content: string): string;
58
+ /**
59
+ * Compute diff between incoming prompts and existing prompts
60
+ * Returns only the changes that need to be made
61
+ */
62
+ export declare function computeDiff(incoming: Array<{
63
+ path: string;
64
+ content: string;
65
+ }>, existing: Document[]): DocumentChange[];
33
66
  export declare function runDocument(path: string, parameters?: Record<string, unknown>, versionUuid?: string): Promise<RunResult>;
34
67
  /**
35
- * Deploy changes to LIVE version
36
- * Creates a draft → pushes changes → publishes to LIVE
68
+ * Deploy changes to LIVE version using the proper workflow:
69
+ * 1. Create a draft version
70
+ * 2. Push all changes to the draft (batch)
71
+ * 3. Publish the draft to make it LIVE
72
+ *
73
+ * This is the same workflow the CLI uses, ensuring proper validation.
37
74
  */
38
75
  export declare function deployToLive(changes: DocumentChange[], versionName?: string): Promise<DeployResult>;
39
76
  export declare function getPromptNames(): Promise<string[]>;
77
+ export {};
package/dist/api.js CHANGED
@@ -22,6 +22,9 @@ exports.listDocuments = listDocuments;
22
22
  exports.getDocument = getDocument;
23
23
  exports.createOrUpdateDocument = createOrUpdateDocument;
24
24
  exports.deleteDocument = deleteDocument;
25
+ exports.pushChanges = pushChanges;
26
+ exports.hashContent = hashContent;
27
+ exports.computeDiff = computeDiff;
25
28
  exports.runDocument = runDocument;
26
29
  exports.deployToLive = deployToLive;
27
30
  exports.getPromptNames = getPromptNames;
@@ -72,7 +75,17 @@ async function request(endpoint, options = {}) {
72
75
  const errorText = await response.text();
73
76
  let errorData;
74
77
  try {
75
- errorData = JSON.parse(errorText);
78
+ const parsed = JSON.parse(errorText);
79
+ // Capture the ENTIRE parsed response as details
80
+ // This ensures we never lose any error information from the API
81
+ const { name, errorCode, code, message, ...rest } = parsed;
82
+ errorData = {
83
+ name: name || 'APIError',
84
+ errorCode: errorCode || code || `HTTP_${response.status}`,
85
+ message: message || `HTTP ${response.status}`,
86
+ // Store ALL remaining fields as details
87
+ details: Object.keys(rest).length > 0 ? rest : undefined,
88
+ };
76
89
  }
77
90
  catch {
78
91
  errorData = {
@@ -81,7 +94,8 @@ async function request(endpoint, options = {}) {
81
94
  message: errorText || `HTTP ${response.status}`,
82
95
  };
83
96
  }
84
- throw new LatitudeApiError(errorData, response.status);
97
+ logger.debug(`API error response: ${errorText}`);
98
+ throw new LatitudeApiError(errorData, response.status, errorText);
85
99
  }
86
100
  // Handle empty responses
87
101
  const contentLength = response.headers.get('content-length');
@@ -113,12 +127,81 @@ async function request(endpoint, options = {}) {
113
127
  // Error Class
114
128
  // ============================================================================
115
129
  class LatitudeApiError extends Error {
116
- constructor(error, statusCode) {
130
+ constructor(error, statusCode, rawResponse) {
117
131
  super(error.message);
118
132
  this.name = error.name;
119
133
  this.errorCode = error.errorCode;
120
134
  this.statusCode = statusCode;
121
135
  this.details = error.details;
136
+ this.rawResponse = rawResponse;
137
+ }
138
+ /**
139
+ * Extract detailed error messages from nested error structures
140
+ */
141
+ getDetailedErrors() {
142
+ const errors = [];
143
+ if (!this.details)
144
+ return errors;
145
+ // Handle errors array in details
146
+ if (Array.isArray(this.details.errors)) {
147
+ for (const err of this.details.errors) {
148
+ if (typeof err === 'string') {
149
+ errors.push(err);
150
+ }
151
+ else if (err && typeof err === 'object') {
152
+ const errObj = err;
153
+ const msg = errObj.message || errObj.error || errObj.detail || JSON.stringify(err);
154
+ const path = errObj.path || errObj.document || errObj.name || '';
155
+ errors.push(path ? `${path}: ${msg}` : String(msg));
156
+ }
157
+ }
158
+ }
159
+ // Handle documents with errors
160
+ if (this.details.documents && typeof this.details.documents === 'object') {
161
+ const docs = this.details.documents;
162
+ for (const [docPath, docInfo] of Object.entries(docs)) {
163
+ if (docInfo && typeof docInfo === 'object') {
164
+ const info = docInfo;
165
+ if (info.error || info.errors) {
166
+ const docErrors = info.errors || [info.error];
167
+ for (const e of Array.isArray(docErrors) ? docErrors : [docErrors]) {
168
+ errors.push(`${docPath}: ${typeof e === 'string' ? e : JSON.stringify(e)}`);
169
+ }
170
+ }
171
+ }
172
+ }
173
+ }
174
+ // Handle validation errors object
175
+ if (this.details.validationErrors && typeof this.details.validationErrors === 'object') {
176
+ const valErrors = this.details.validationErrors;
177
+ for (const [field, fieldErrors] of Object.entries(valErrors)) {
178
+ if (Array.isArray(fieldErrors)) {
179
+ for (const fe of fieldErrors) {
180
+ errors.push(`${field}: ${typeof fe === 'string' ? fe : JSON.stringify(fe)}`);
181
+ }
182
+ }
183
+ else {
184
+ errors.push(`${field}: ${typeof fieldErrors === 'string' ? fieldErrors : JSON.stringify(fieldErrors)}`);
185
+ }
186
+ }
187
+ }
188
+ // Handle cause field
189
+ if (this.details.cause) {
190
+ const cause = this.details.cause;
191
+ if (typeof cause === 'string') {
192
+ errors.push(cause);
193
+ }
194
+ else if (typeof cause === 'object') {
195
+ const causeObj = cause;
196
+ if (causeObj.message) {
197
+ errors.push(String(causeObj.message));
198
+ }
199
+ else {
200
+ errors.push(JSON.stringify(cause));
201
+ }
202
+ }
203
+ }
204
+ return errors;
122
205
  }
123
206
  toMarkdown() {
124
207
  let md = `## ❌ Error: ${this.name}\n\n`;
@@ -127,11 +210,45 @@ class LatitudeApiError extends Error {
127
210
  if (this.statusCode) {
128
211
  md += `\n**HTTP Status:** ${this.statusCode}\n`;
129
212
  }
213
+ // Try to extract detailed errors first
214
+ const detailedErrors = this.getDetailedErrors();
215
+ if (detailedErrors.length > 0) {
216
+ md += `\n**Detailed Errors (${detailedErrors.length}):**\n`;
217
+ for (const err of detailedErrors) {
218
+ md += `- ${err}\n`;
219
+ }
220
+ }
221
+ // Always show details if present (structured data from API)
130
222
  if (this.details && Object.keys(this.details).length > 0) {
131
- md += `\n**Details:**\n\`\`\`json\n${JSON.stringify(this.details, null, 2)}\n\`\`\`\n`;
223
+ md += `\n**API Response Details:**\n\`\`\`json\n${JSON.stringify(this.details, null, 2)}\n\`\`\`\n`;
224
+ }
225
+ // Always show raw response if available (for debugging)
226
+ if (this.rawResponse && this.rawResponse !== JSON.stringify(this.details)) {
227
+ md += `\n**Raw API Response:**\n\`\`\`json\n${this.rawResponse}\n\`\`\`\n`;
132
228
  }
133
229
  return md;
134
230
  }
231
+ /**
232
+ * Get a concise error message suitable for per-prompt error tracking
233
+ */
234
+ getConciseMessage() {
235
+ const detailed = this.getDetailedErrors();
236
+ if (detailed.length > 0) {
237
+ return detailed.join('; ');
238
+ }
239
+ // If no detailed errors, include details summary if available
240
+ if (this.details && Object.keys(this.details).length > 0) {
241
+ return `${this.message} | Details: ${JSON.stringify(this.details)}`;
242
+ }
243
+ // Last resort: include raw response snippet
244
+ if (this.rawResponse) {
245
+ const snippet = this.rawResponse.length > 200
246
+ ? this.rawResponse.substring(0, 200) + '...'
247
+ : this.rawResponse;
248
+ return `${this.message} | Raw: ${snippet}`;
249
+ }
250
+ return this.message;
251
+ }
135
252
  }
136
253
  exports.LatitudeApiError = LatitudeApiError;
137
254
  // ============================================================================
@@ -171,12 +288,12 @@ async function getDocument(path, versionUuid = 'live') {
171
288
  const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
172
289
  return request(`/projects/${projectId}/versions/${versionUuid}/documents/${normalizedPath}`);
173
290
  }
174
- async function createOrUpdateDocument(versionUuid, path, content) {
291
+ async function createOrUpdateDocument(versionUuid, path, content, force = false) {
175
292
  const projectId = getProjectId();
176
- logger.debug(`Creating/updating document: ${path} (${content.length} chars)`);
293
+ logger.debug(`Creating/updating document: ${path} (${content.length} chars, force=${force})`);
177
294
  return request(`/projects/${projectId}/versions/${versionUuid}/documents/create-or-update`, {
178
295
  method: 'POST',
179
- body: { path, prompt: content },
296
+ body: { path, prompt: content, force },
180
297
  });
181
298
  }
182
299
  async function deleteDocument(versionUuid, path) {
@@ -184,6 +301,78 @@ async function deleteDocument(versionUuid, path) {
184
301
  const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
185
302
  await request(`/projects/${projectId}/versions/${versionUuid}/documents/${normalizedPath}`, { method: 'DELETE' });
186
303
  }
304
+ /**
305
+ * Push changes to a version in a single batch
306
+ * This is the CLI-style push that sends all changes at once
307
+ */
308
+ async function pushChanges(versionUuid, changes) {
309
+ const projectId = getProjectId();
310
+ // Format changes for the API
311
+ const apiChanges = changes.map((c) => ({
312
+ path: c.path,
313
+ content: c.content || '',
314
+ status: c.status,
315
+ }));
316
+ logger.info(`Pushing ${changes.length} change(s) to version ${versionUuid}`);
317
+ return request(`/projects/${projectId}/versions/${versionUuid}/push`, {
318
+ method: 'POST',
319
+ body: { changes: apiChanges },
320
+ });
321
+ }
322
+ /**
323
+ * Compute hash of content for diff comparison
324
+ */
325
+ function hashContent(content) {
326
+ // Simple hash - in production you might want crypto
327
+ let hash = 0;
328
+ for (let i = 0; i < content.length; i++) {
329
+ const char = content.charCodeAt(i);
330
+ hash = ((hash << 5) - hash) + char;
331
+ hash = hash & hash; // Convert to 32bit integer
332
+ }
333
+ return hash.toString(16);
334
+ }
335
+ /**
336
+ * Compute diff between incoming prompts and existing prompts
337
+ * Returns only the changes that need to be made
338
+ */
339
+ function computeDiff(incoming, existing) {
340
+ const changes = [];
341
+ const existingMap = new Map(existing.map((d) => [d.path, d]));
342
+ const incomingPaths = new Set(incoming.map((p) => p.path));
343
+ // Check each incoming prompt
344
+ for (const prompt of incoming) {
345
+ const existingDoc = existingMap.get(prompt.path);
346
+ if (!existingDoc) {
347
+ // New prompt
348
+ changes.push({
349
+ path: prompt.path,
350
+ content: prompt.content,
351
+ status: 'added',
352
+ });
353
+ }
354
+ else if (existingDoc.content !== prompt.content) {
355
+ // Modified prompt
356
+ changes.push({
357
+ path: prompt.path,
358
+ content: prompt.content,
359
+ status: 'modified',
360
+ });
361
+ }
362
+ // If content is same, no change needed (don't include in changes)
363
+ }
364
+ // Check for deleted prompts (exist remotely but not in incoming)
365
+ for (const path of existingMap.keys()) {
366
+ if (!incomingPaths.has(path)) {
367
+ changes.push({
368
+ path,
369
+ content: '',
370
+ status: 'deleted',
371
+ });
372
+ }
373
+ }
374
+ return changes;
375
+ }
187
376
  async function runDocument(path, parameters, versionUuid = 'live') {
188
377
  const projectId = getProjectId();
189
378
  return request(`/projects/${projectId}/versions/${versionUuid}/documents/run`, {
@@ -197,10 +386,10 @@ async function runDocument(path, parameters, versionUuid = 'live') {
197
386
  });
198
387
  }
199
388
  /**
200
- * Format timestamp in San Francisco time: "14 Jan 2025 - 13:11"
201
- * Optionally prepend action prefix like "append research-validate"
389
+ * Format timestamp for version names: "14 Jan 2025 - 13:11"
390
+ * Optionally prepend action prefix like "push cover-letter"
202
391
  */
203
- function formatSFTimestamp(prefix) {
392
+ function formatVersionName(prefix) {
204
393
  const now = new Date();
205
394
  const options = {
206
395
  timeZone: 'America/Los_Angeles',
@@ -214,53 +403,79 @@ function formatSFTimestamp(prefix) {
214
403
  const formatted = now.toLocaleString('en-US', options);
215
404
  // "Jan 14, 2025, 13:11" → "14 Jan 2025 - 13:11"
216
405
  const match = formatted.match(/(\w+)\s+(\d+),\s+(\d+),\s+(\d+:\d+)/);
217
- let timestamp = match
406
+ const timestamp = match
218
407
  ? `${match[2]} ${match[1]} ${match[3]} - ${match[4]}`
219
408
  : now.toISOString().replace(/[:.]/g, '-');
220
- if (prefix) {
221
- return `${prefix} (${timestamp})`;
222
- }
223
- return timestamp;
409
+ return prefix ? `${prefix} (${timestamp})` : timestamp;
224
410
  }
225
411
  /**
226
- * Push changes to a version using the push endpoint
412
+ * Create a synthetic Version object for no-op deploys.
413
+ * Returns a fully populated Version with all required fields.
227
414
  */
228
- async function pushChangesToVersion(versionUuid, changes) {
229
- const projectId = getProjectId();
230
- const apiChanges = changes.map((c) => ({
231
- path: c.path,
232
- content: c.content,
233
- status: c.status,
234
- }));
235
- logger.debug(`Pushing ${changes.length} change(s) to version ${versionUuid}`);
236
- await request(`/projects/${projectId}/versions/${versionUuid}/push`, {
237
- method: 'POST',
238
- body: { changes: apiChanges },
239
- });
415
+ function createNoOpVersion() {
416
+ const now = new Date().toISOString();
417
+ return {
418
+ id: 0,
419
+ uuid: 'live',
420
+ projectId: 0,
421
+ message: 'No changes to deploy',
422
+ createdAt: now,
423
+ updatedAt: now,
424
+ status: 'live',
425
+ };
240
426
  }
241
427
  /**
242
- * Deploy changes to LIVE version
243
- * Creates a draft → pushes changes → publishes to LIVE
428
+ * Deploy changes to LIVE version using the proper workflow:
429
+ * 1. Create a draft version
430
+ * 2. Push all changes to the draft (batch)
431
+ * 3. Publish the draft to make it LIVE
432
+ *
433
+ * This is the same workflow the CLI uses, ensuring proper validation.
244
434
  */
245
435
  async function deployToLive(changes, versionName) {
246
- const name = versionName || formatSFTimestamp();
436
+ if (changes.length === 0) {
437
+ logger.info('No changes to deploy');
438
+ return {
439
+ version: createNoOpVersion(),
440
+ documentsProcessed: 0,
441
+ added: [],
442
+ modified: [],
443
+ deleted: [],
444
+ };
445
+ }
446
+ // Filter out unchanged items (they shouldn't be sent to API)
447
+ const actualChanges = changes.filter(c => c.status !== 'unchanged');
448
+ if (actualChanges.length === 0) {
449
+ logger.info('All prompts are unchanged, nothing to deploy');
450
+ return {
451
+ version: createNoOpVersion(),
452
+ documentsProcessed: 0,
453
+ added: [],
454
+ modified: [],
455
+ deleted: [],
456
+ };
457
+ }
458
+ // Categorize changes for logging and return value
459
+ const added = actualChanges.filter((c) => c.status === 'added').map((c) => c.path);
460
+ const modified = actualChanges.filter((c) => c.status === 'modified').map((c) => c.path);
461
+ const deleted = actualChanges.filter((c) => c.status === 'deleted').map((c) => c.path);
462
+ const name = formatVersionName(versionName);
463
+ logger.info(`Deploying to LIVE: ${added.length} added, ${modified.length} modified, ${deleted.length} deleted`);
247
464
  // Step 1: Create a new draft version
248
- logger.info(`Creating draft: ${name}`);
249
- const version = await createVersion(name);
250
- // Step 2: Push all changes to the draft using the push endpoint
251
- logger.info(`Pushing ${changes.length} change(s) to draft...`);
252
- await pushChangesToVersion(version.uuid, changes);
253
- // Step 3: Publish the draft to LIVE
254
- logger.info(`Publishing version ${version.uuid} to LIVE with title: ${name}`);
255
- const published = await publishVersion(version.uuid, name);
256
- logger.info(`Published! Version is now LIVE: ${published.uuid}`);
257
- // Categorize changes for return value
258
- const added = changes.filter((c) => c.status === 'added').map((c) => c.path);
259
- const modified = changes.filter((c) => c.status === 'modified').map((c) => c.path);
260
- const deleted = changes.filter((c) => c.status === 'deleted').map((c) => c.path);
465
+ logger.info(`Creating draft version: ${name}`);
466
+ const draft = await createVersion(name);
467
+ logger.info(`Draft created: ${draft.uuid}`);
468
+ // Step 2: Push all changes to the draft in ONE batch
469
+ logger.info(`Pushing ${actualChanges.length} change(s) to draft...`);
470
+ const pushResult = await pushChanges(draft.uuid, actualChanges);
471
+ logger.info(`Push complete: ${pushResult.documentsProcessed} documents processed`);
472
+ // Step 3: Publish the draft to make it LIVE
473
+ logger.info(`Publishing draft ${draft.uuid} to LIVE...`);
474
+ const published = await publishVersion(draft.uuid, name);
475
+ logger.info(`Published successfully! Version is now LIVE: ${published.uuid}`);
261
476
  return {
262
477
  version: published,
263
- documentsProcessed: changes.length,
478
+ documentsProcessed: pushResult.documentsProcessed,
264
479
  added,
265
480
  modified,
266
481
  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.6",
3
+ "version": "2.2.1",
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",