latitude-mcp-server 3.2.2 → 3.3.0
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 +19 -2
- package/dist/api.js +87 -23
- package/dist/tools.js +34 -20
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
- package/.releaserc.json +0 -34
- package/PROMPT_GUIDE.md +0 -380
- package/eslint.config.mjs +0 -46
- package/openapi.json +0 -12592
- package/prompts/cover-letter-generate.promptl +0 -71
- package/prompts/cv-ingest-questions.promptl +0 -386
- package/prompts/cv-ingest.promptl +0 -449
- package/prompts/job-filter-bootstrap.promptl +0 -115
- package/prompts/job-filter-refine.promptl +0 -173
- package/prompts/linkedin-search.promptl +0 -225
- package/prompts/pattern-bootstrap.promptl +0 -2753
- package/prompts/pattern-refine.promptl +0 -247
- package/prompts/question-generate.promptl +0 -172
- package/prompts/research-discover.promptl +0 -235
- package/prompts/research-validate.promptl +0 -193
package/dist/api.d.ts
CHANGED
|
@@ -33,6 +33,10 @@ export declare class LatitudeApiError extends Error {
|
|
|
33
33
|
*/
|
|
34
34
|
export declare function getProjectId(): string;
|
|
35
35
|
export declare function listDocuments(versionUuid?: string): Promise<Document[]>;
|
|
36
|
+
/**
|
|
37
|
+
* Compute SHA-256 hash of content (matches Latitude's contentHash)
|
|
38
|
+
*/
|
|
39
|
+
export declare function computeContentHash(content: string): string;
|
|
36
40
|
export declare function getDocument(path: string, versionUuid?: string): Promise<Document>;
|
|
37
41
|
/**
|
|
38
42
|
* Push response from the API
|
|
@@ -42,13 +46,26 @@ interface PushResponse {
|
|
|
42
46
|
documentsProcessed: number;
|
|
43
47
|
}
|
|
44
48
|
/**
|
|
45
|
-
* Push changes to a version
|
|
46
|
-
*
|
|
49
|
+
* Push changes to a version
|
|
50
|
+
*
|
|
51
|
+
* IMPORTANT: The Latitude API /push endpoint has a bug where 'modified' status
|
|
52
|
+
* fails with "A document with the same path already exists" error for inherited
|
|
53
|
+
* documents in drafts. Workaround: for modified docs, we first delete then add
|
|
54
|
+
* in sequential push calls.
|
|
47
55
|
*/
|
|
48
56
|
export declare function pushChanges(versionUuid: string, changes: DocumentChange[]): Promise<PushResponse>;
|
|
57
|
+
/**
|
|
58
|
+
* Normalize document path for consistent comparison.
|
|
59
|
+
* Handles leading/trailing slashes, whitespace, and ensures consistent format.
|
|
60
|
+
* API may return paths with or without leading slash - this normalizes them.
|
|
61
|
+
*/
|
|
62
|
+
export declare function normalizePath(path: string): string;
|
|
49
63
|
/**
|
|
50
64
|
* Compute diff between incoming prompts and existing prompts
|
|
51
65
|
* Returns only the changes that need to be made
|
|
66
|
+
*
|
|
67
|
+
* IMPORTANT: Uses normalized paths and content hashes for fast comparison.
|
|
68
|
+
* Handles API inconsistencies where paths may have leading slashes.
|
|
52
69
|
*/
|
|
53
70
|
export declare function computeDiff(incoming: Array<{
|
|
54
71
|
path: string;
|
package/dist/api.js
CHANGED
|
@@ -16,12 +16,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
16
16
|
exports.LatitudeApiError = void 0;
|
|
17
17
|
exports.getProjectId = getProjectId;
|
|
18
18
|
exports.listDocuments = listDocuments;
|
|
19
|
+
exports.computeContentHash = computeContentHash;
|
|
19
20
|
exports.getDocument = getDocument;
|
|
20
21
|
exports.pushChanges = pushChanges;
|
|
22
|
+
exports.normalizePath = normalizePath;
|
|
21
23
|
exports.computeDiff = computeDiff;
|
|
22
24
|
exports.runDocument = runDocument;
|
|
23
25
|
exports.validatePromptLContent = validatePromptLContent;
|
|
24
26
|
exports.deployToLive = deployToLive;
|
|
27
|
+
const crypto_1 = require("crypto");
|
|
25
28
|
const logger_util_js_1 = require("./utils/logger.util.js");
|
|
26
29
|
const config_util_js_1 = require("./utils/config.util.js");
|
|
27
30
|
const promptl_ai_1 = require("promptl-ai");
|
|
@@ -278,40 +281,97 @@ async function listDocuments(versionUuid = 'live') {
|
|
|
278
281
|
const projectId = getProjectId();
|
|
279
282
|
return request(`/projects/${projectId}/versions/${versionUuid}/documents`);
|
|
280
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* Compute SHA-256 hash of content (matches Latitude's contentHash)
|
|
286
|
+
*/
|
|
287
|
+
function computeContentHash(content) {
|
|
288
|
+
return (0, crypto_1.createHash)('sha256').update(content).digest('hex');
|
|
289
|
+
}
|
|
281
290
|
async function getDocument(path, versionUuid = 'live') {
|
|
282
291
|
const projectId = getProjectId();
|
|
283
292
|
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
284
293
|
return request(`/projects/${projectId}/versions/${versionUuid}/documents/${normalizedPath}`);
|
|
285
294
|
}
|
|
286
295
|
/**
|
|
287
|
-
* Push changes to a version
|
|
288
|
-
*
|
|
296
|
+
* Push changes to a version
|
|
297
|
+
*
|
|
298
|
+
* IMPORTANT: The Latitude API /push endpoint has a bug where 'modified' status
|
|
299
|
+
* fails with "A document with the same path already exists" error for inherited
|
|
300
|
+
* documents in drafts. Workaround: for modified docs, we first delete then add
|
|
301
|
+
* in sequential push calls.
|
|
289
302
|
*/
|
|
290
303
|
async function pushChanges(versionUuid, changes) {
|
|
291
304
|
const projectId = getProjectId();
|
|
292
|
-
//
|
|
293
|
-
const
|
|
305
|
+
// Separate changes by type
|
|
306
|
+
const modified = changes.filter((c) => c.status === 'modified');
|
|
307
|
+
const nonModified = changes.filter((c) => c.status !== 'modified');
|
|
308
|
+
let totalProcessed = 0;
|
|
309
|
+
// Step 1: If there are modified docs, delete them first
|
|
310
|
+
if (modified.length > 0) {
|
|
311
|
+
const deleteChanges = modified.map((c) => ({
|
|
312
|
+
path: c.path,
|
|
313
|
+
content: '',
|
|
314
|
+
status: 'deleted',
|
|
315
|
+
}));
|
|
316
|
+
logger.info(`Deleting ${modified.length} modified doc(s) before re-adding...`);
|
|
317
|
+
await request(`/projects/${projectId}/versions/${versionUuid}/push`, {
|
|
318
|
+
method: 'POST',
|
|
319
|
+
body: { changes: deleteChanges },
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
// Step 2: Push all adds (including modified-as-add) and deletes
|
|
323
|
+
const addChanges = modified.map((c) => ({
|
|
324
|
+
path: c.path,
|
|
325
|
+
content: c.content || '',
|
|
326
|
+
status: 'added',
|
|
327
|
+
}));
|
|
328
|
+
const otherChanges = nonModified.map((c) => ({
|
|
294
329
|
path: c.path,
|
|
295
330
|
content: c.content || '',
|
|
296
331
|
status: c.status,
|
|
297
332
|
}));
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
333
|
+
const allChanges = [...addChanges, ...otherChanges];
|
|
334
|
+
if (allChanges.length > 0) {
|
|
335
|
+
logger.info(`Pushing ${allChanges.length} change(s) to version ${versionUuid}`);
|
|
336
|
+
const result = await request(`/projects/${projectId}/versions/${versionUuid}/push`, {
|
|
337
|
+
method: 'POST',
|
|
338
|
+
body: { changes: allChanges },
|
|
339
|
+
});
|
|
340
|
+
totalProcessed = result.documentsProcessed;
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
versionUuid,
|
|
344
|
+
documentsProcessed: totalProcessed || changes.length,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Normalize document path for consistent comparison.
|
|
349
|
+
* Handles leading/trailing slashes, whitespace, and ensures consistent format.
|
|
350
|
+
* API may return paths with or without leading slash - this normalizes them.
|
|
351
|
+
*/
|
|
352
|
+
function normalizePath(path) {
|
|
353
|
+
return path
|
|
354
|
+
.trim()
|
|
355
|
+
.replace(/^\/+/, '') // Remove leading slashes
|
|
356
|
+
.replace(/\/+$/, '') // Remove trailing slashes
|
|
357
|
+
.replace(/\/+/g, '/'); // Collapse multiple slashes
|
|
303
358
|
}
|
|
304
359
|
/**
|
|
305
360
|
* Compute diff between incoming prompts and existing prompts
|
|
306
361
|
* Returns only the changes that need to be made
|
|
362
|
+
*
|
|
363
|
+
* IMPORTANT: Uses normalized paths and content hashes for fast comparison.
|
|
364
|
+
* Handles API inconsistencies where paths may have leading slashes.
|
|
307
365
|
*/
|
|
308
366
|
function computeDiff(incoming, existing) {
|
|
309
367
|
const changes = [];
|
|
310
|
-
|
|
311
|
-
const
|
|
368
|
+
// Build map with NORMALIZED paths as keys, storing path + contentHash for fast comparison
|
|
369
|
+
const existingMap = new Map(existing.map((d) => [normalizePath(d.path), { path: d.path, contentHash: d.contentHash }]));
|
|
370
|
+
const incomingPaths = new Set(incoming.map((p) => normalizePath(p.path)));
|
|
312
371
|
// Check each incoming prompt
|
|
313
372
|
for (const prompt of incoming) {
|
|
314
|
-
const
|
|
373
|
+
const normalizedPath = normalizePath(prompt.path);
|
|
374
|
+
const existingDoc = existingMap.get(normalizedPath);
|
|
315
375
|
if (!existingDoc) {
|
|
316
376
|
// New prompt
|
|
317
377
|
changes.push({
|
|
@@ -320,21 +380,25 @@ function computeDiff(incoming, existing) {
|
|
|
320
380
|
status: 'added',
|
|
321
381
|
});
|
|
322
382
|
}
|
|
323
|
-
else
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
383
|
+
else {
|
|
384
|
+
// Compare hashes for speed (avoid comparing large content strings)
|
|
385
|
+
const localHash = computeContentHash(prompt.content);
|
|
386
|
+
if (existingDoc.contentHash !== localHash) {
|
|
387
|
+
// Modified prompt - use the EXISTING path to ensure API compatibility
|
|
388
|
+
changes.push({
|
|
389
|
+
path: existingDoc.path,
|
|
390
|
+
content: prompt.content,
|
|
391
|
+
status: 'modified',
|
|
392
|
+
});
|
|
393
|
+
}
|
|
330
394
|
}
|
|
331
|
-
// If
|
|
395
|
+
// If same hash, no change needed (don't include in changes)
|
|
332
396
|
}
|
|
333
397
|
// Check for deleted prompts (exist remotely but not in incoming)
|
|
334
|
-
for (const
|
|
335
|
-
if (!incomingPaths.has(
|
|
398
|
+
for (const [normalizedExistingPath, doc] of existingMap.entries()) {
|
|
399
|
+
if (!incomingPaths.has(normalizedExistingPath)) {
|
|
336
400
|
changes.push({
|
|
337
|
-
path,
|
|
401
|
+
path: doc.path,
|
|
338
402
|
content: '',
|
|
339
403
|
status: 'deleted',
|
|
340
404
|
});
|
package/dist/tools.js
CHANGED
|
@@ -28,6 +28,8 @@ async function refreshPromptCache() {
|
|
|
28
28
|
try {
|
|
29
29
|
const docs = await (0, api_js_1.listDocuments)('live');
|
|
30
30
|
cachedPromptNames = docs.map((d) => d.path);
|
|
31
|
+
// Also cache full documents for variable extraction (avoids extra API calls)
|
|
32
|
+
cachedDocuments = docs.map((d) => ({ path: d.path, content: d.content }));
|
|
31
33
|
cacheLastUpdated = new Date();
|
|
32
34
|
logger.debug(`Cache updated: ${cachedPromptNames.length} prompts`);
|
|
33
35
|
return cachedPromptNames;
|
|
@@ -128,10 +130,14 @@ function extractVariables(content) {
|
|
|
128
130
|
}
|
|
129
131
|
return Array.from(variables);
|
|
130
132
|
}
|
|
133
|
+
// Cache for full documents (path + content) to avoid repeated API calls
|
|
134
|
+
let cachedDocuments = [];
|
|
131
135
|
/**
|
|
132
136
|
* Build dynamic description for run_prompt with prompt names and their variables
|
|
137
|
+
* Uses cached documents to avoid individual API calls per prompt
|
|
133
138
|
*/
|
|
134
139
|
async function buildRunPromptDescription() {
|
|
140
|
+
// Use cached documents if available, otherwise just show names
|
|
135
141
|
const names = await getCachedPromptNames();
|
|
136
142
|
let desc = 'Execute a prompt with parameters.';
|
|
137
143
|
if (names.length === 0) {
|
|
@@ -139,12 +145,13 @@ async function buildRunPromptDescription() {
|
|
|
139
145
|
return desc;
|
|
140
146
|
}
|
|
141
147
|
desc += `\n\n**Available prompts (${names.length}):**`;
|
|
142
|
-
//
|
|
148
|
+
// Use cached documents for variable extraction (no extra API calls)
|
|
143
149
|
const maxToShow = Math.min(names.length, 10);
|
|
150
|
+
const docMap = new Map(cachedDocuments.map(d => [d.path, d.content]));
|
|
144
151
|
for (let i = 0; i < maxToShow; i++) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const vars = extractVariables(
|
|
152
|
+
const content = docMap.get(names[i]);
|
|
153
|
+
if (content) {
|
|
154
|
+
const vars = extractVariables(content);
|
|
148
155
|
if (vars.length > 0) {
|
|
149
156
|
desc += `\n- \`${names[i]}\` (params: ${vars.map(v => `\`${v}\``).join(', ')})`;
|
|
150
157
|
}
|
|
@@ -152,7 +159,7 @@ async function buildRunPromptDescription() {
|
|
|
152
159
|
desc += `\n- \`${names[i]}\` (no params)`;
|
|
153
160
|
}
|
|
154
161
|
}
|
|
155
|
-
|
|
162
|
+
else {
|
|
156
163
|
desc += `\n- \`${names[i]}\``;
|
|
157
164
|
}
|
|
158
165
|
}
|
|
@@ -332,9 +339,10 @@ async function handlePushPrompts(args) {
|
|
|
332
339
|
const modified = changes.filter((c) => c.status === 'modified');
|
|
333
340
|
const deleted = changes.filter((c) => c.status === 'deleted');
|
|
334
341
|
if (changes.length === 0) {
|
|
335
|
-
|
|
342
|
+
// No changes - reuse existingDocs instead of another API call
|
|
343
|
+
const currentNames = existingDocs.map(d => d.path);
|
|
336
344
|
return formatSuccess('No Changes Needed', `All ${prompts.length} prompt(s) are already up to date.\n\n` +
|
|
337
|
-
`**Current LIVE prompts (${
|
|
345
|
+
`**Current LIVE prompts (${currentNames.length}):** ${currentNames.map(n => `\`${n}\``).join(', ')}`);
|
|
338
346
|
}
|
|
339
347
|
// Push all changes in one batch
|
|
340
348
|
try {
|
|
@@ -415,23 +423,29 @@ async function handleAddPrompt(args) {
|
|
|
415
423
|
return formatValidationErrors(validation.errors);
|
|
416
424
|
}
|
|
417
425
|
logger.info(`All ${prompts.length} prompt(s) passed validation`);
|
|
418
|
-
// Get existing prompts
|
|
426
|
+
// Get existing prompts (includes contentHash for fast comparison)
|
|
419
427
|
const existingDocs = await (0, api_js_1.listDocuments)('live');
|
|
420
|
-
|
|
428
|
+
// Use NORMALIZED paths as keys for reliable matching (handles /path vs path inconsistencies)
|
|
429
|
+
// Map to { path, contentHash } for fast hash-based comparison
|
|
430
|
+
const existingMap = new Map(existingDocs.map((d) => [(0, api_js_1.normalizePath)(d.path), { path: d.path, contentHash: d.contentHash }]));
|
|
421
431
|
// Build changes - ALWAYS overwrite if exists, add if new, NEVER delete
|
|
432
|
+
// Uses hash comparison for speed (avoids comparing 100KB+ content strings)
|
|
422
433
|
const changes = [];
|
|
423
434
|
for (const prompt of prompts) {
|
|
424
|
-
const
|
|
435
|
+
const normalizedName = (0, api_js_1.normalizePath)(prompt.name);
|
|
436
|
+
const existingDoc = existingMap.get(normalizedName);
|
|
425
437
|
if (existingDoc) {
|
|
426
|
-
//
|
|
427
|
-
|
|
438
|
+
// Compare hashes instead of full content (much faster)
|
|
439
|
+
const localHash = (0, api_js_1.computeContentHash)(prompt.content);
|
|
440
|
+
if (existingDoc.contentHash !== localHash) {
|
|
441
|
+
// Use the EXISTING path from API to ensure compatibility
|
|
428
442
|
changes.push({
|
|
429
|
-
path:
|
|
443
|
+
path: existingDoc.path,
|
|
430
444
|
content: prompt.content,
|
|
431
445
|
status: 'modified',
|
|
432
446
|
});
|
|
433
447
|
}
|
|
434
|
-
// If same
|
|
448
|
+
// If same hash, skip silently (unchanged)
|
|
435
449
|
}
|
|
436
450
|
else {
|
|
437
451
|
// New prompt
|
|
@@ -446,9 +460,10 @@ async function handleAddPrompt(args) {
|
|
|
446
460
|
const added = changes.filter((c) => c.status === 'added');
|
|
447
461
|
const modified = changes.filter((c) => c.status === 'modified');
|
|
448
462
|
if (changes.length === 0) {
|
|
449
|
-
|
|
463
|
+
// No changes - reuse existingDocs instead of another API call
|
|
464
|
+
const currentNames = existingDocs.map(d => d.path);
|
|
450
465
|
return formatSuccess('No Changes Needed', `All ${prompts.length} prompt(s) are already up to date.\n\n` +
|
|
451
|
-
`**Current LIVE prompts (${
|
|
466
|
+
`**Current LIVE prompts (${currentNames.length}):** ${currentNames.map(n => `\`${n}\``).join(', ')}`);
|
|
452
467
|
}
|
|
453
468
|
// Push all changes in one batch
|
|
454
469
|
try {
|
|
@@ -501,18 +516,17 @@ async function handlePullPrompts(args) {
|
|
|
501
516
|
for (const file of existingFiles) {
|
|
502
517
|
(0, fs_1.unlinkSync)((0, path_1.join)(outputDir, file));
|
|
503
518
|
}
|
|
504
|
-
// Get all prompts from LIVE
|
|
519
|
+
// Get all prompts from LIVE (includes full content)
|
|
505
520
|
const docs = await (0, api_js_1.listDocuments)('live');
|
|
506
521
|
if (docs.length === 0) {
|
|
507
522
|
return formatSuccess('No Prompts to Pull', 'The project has no prompts.');
|
|
508
523
|
}
|
|
509
|
-
//
|
|
524
|
+
// Write files directly - listDocuments already returns full content
|
|
510
525
|
const written = [];
|
|
511
526
|
for (const doc of docs) {
|
|
512
|
-
const fullDoc = await (0, api_js_1.getDocument)(doc.path, 'live');
|
|
513
527
|
const filename = `${doc.path.replace(/\//g, '_')}.promptl`;
|
|
514
528
|
const filepath = (0, path_1.join)(outputDir, filename);
|
|
515
|
-
(0, fs_1.writeFileSync)(filepath,
|
|
529
|
+
(0, fs_1.writeFileSync)(filepath, doc.content, 'utf-8');
|
|
516
530
|
written.push(filename);
|
|
517
531
|
}
|
|
518
532
|
// Update cache
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "latitude-mcp-server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
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",
|
package/.releaserc.json
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"branches": ["main"],
|
|
3
|
-
"plugins": [
|
|
4
|
-
"@semantic-release/commit-analyzer",
|
|
5
|
-
"@semantic-release/release-notes-generator",
|
|
6
|
-
"@semantic-release/changelog",
|
|
7
|
-
[
|
|
8
|
-
"@semantic-release/exec",
|
|
9
|
-
{
|
|
10
|
-
"prepareCmd": "node scripts/update-version.js ${nextRelease.version} && npm run build && chmod +x dist/index.js"
|
|
11
|
-
}
|
|
12
|
-
],
|
|
13
|
-
[
|
|
14
|
-
"@semantic-release/npm",
|
|
15
|
-
{
|
|
16
|
-
"npmPublish": true,
|
|
17
|
-
"pkgRoot": "."
|
|
18
|
-
}
|
|
19
|
-
],
|
|
20
|
-
[
|
|
21
|
-
"@semantic-release/git",
|
|
22
|
-
{
|
|
23
|
-
"assets": [
|
|
24
|
-
"package.json",
|
|
25
|
-
"CHANGELOG.md",
|
|
26
|
-
"src/index.ts",
|
|
27
|
-
"src/cli/index.ts"
|
|
28
|
-
],
|
|
29
|
-
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
|
30
|
-
}
|
|
31
|
-
],
|
|
32
|
-
"@semantic-release/github"
|
|
33
|
-
]
|
|
34
|
-
}
|