latitude-mcp-server 2.1.7 → 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 +42 -4
- package/dist/api.js +280 -40
- package/dist/tools.js +107 -87
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
|
@@ -30,10 +39,39 @@ export declare function listDocuments(versionUuid?: string): Promise<Document[]>
|
|
|
30
39
|
export declare function getDocument(path: string, versionUuid?: string): Promise<Document>;
|
|
31
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
|
|
36
|
-
*
|
|
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
|
-
export declare function deployToLive(changes: DocumentChange[],
|
|
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
|
-
|
|
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
|
-
|
|
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
|
// ============================================================================
|
|
@@ -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,45 +386,96 @@ async function runDocument(path, parameters, versionUuid = 'live') {
|
|
|
197
386
|
});
|
|
198
387
|
}
|
|
199
388
|
/**
|
|
200
|
-
*
|
|
201
|
-
*
|
|
389
|
+
* Format timestamp for version names: "14 Jan 2025 - 13:11"
|
|
390
|
+
* Optionally prepend action prefix like "push cover-letter"
|
|
202
391
|
*/
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
392
|
+
function formatVersionName(prefix) {
|
|
393
|
+
const now = new Date();
|
|
394
|
+
const options = {
|
|
395
|
+
timeZone: 'America/Los_Angeles',
|
|
396
|
+
day: 'numeric',
|
|
397
|
+
month: 'short',
|
|
398
|
+
year: 'numeric',
|
|
399
|
+
hour: '2-digit',
|
|
400
|
+
minute: '2-digit',
|
|
401
|
+
hour12: false,
|
|
402
|
+
};
|
|
403
|
+
const formatted = now.toLocaleString('en-US', options);
|
|
404
|
+
// "Jan 14, 2025, 13:11" → "14 Jan 2025 - 13:11"
|
|
405
|
+
const match = formatted.match(/(\w+)\s+(\d+),\s+(\d+),\s+(\d+:\d+)/);
|
|
406
|
+
const timestamp = match
|
|
407
|
+
? `${match[2]} ${match[1]} ${match[3]} - ${match[4]}`
|
|
408
|
+
: now.toISOString().replace(/[:.]/g, '-');
|
|
409
|
+
return prefix ? `${prefix} (${timestamp})` : timestamp;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Create a synthetic Version object for no-op deploys.
|
|
413
|
+
* Returns a fully populated Version with all required fields.
|
|
414
|
+
*/
|
|
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
|
+
};
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
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.
|
|
434
|
+
*/
|
|
435
|
+
async function deployToLive(changes, versionName) {
|
|
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
|
+
};
|
|
232
457
|
}
|
|
233
|
-
//
|
|
234
|
-
const
|
|
235
|
-
const
|
|
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`);
|
|
464
|
+
// Step 1: Create a new draft version
|
|
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}`);
|
|
236
476
|
return {
|
|
237
|
-
version:
|
|
238
|
-
documentsProcessed:
|
|
477
|
+
version: published,
|
|
478
|
+
documentsProcessed: pushResult.documentsProcessed,
|
|
239
479
|
added,
|
|
240
480
|
modified,
|
|
241
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
|
|
197
|
+
// Get existing prompts for diff computation
|
|
198
198
|
const existingDocs = await (0, api_js_1.listDocuments)('live');
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
|
286
|
-
//
|
|
287
|
-
const
|
|
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
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
303
|
+
skipped.push(prompt.name);
|
|
312
304
|
}
|
|
313
305
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
//
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
if (
|
|
323
|
-
|
|
324
|
-
|
|
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 (
|
|
331
|
-
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
|
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",
|