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 +42 -3
- package/dist/api.js +270 -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,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
|
-
|
|
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
|
|
36
|
-
*
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
201
|
-
*
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
//
|
|
234
|
-
const
|
|
235
|
-
const
|
|
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:
|
|
238
|
-
documentsProcessed:
|
|
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
|
|
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.
|
|
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",
|