@juicedresume/mcp 0.2.2 → 0.3.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/index.js +72 -11
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -215,6 +215,23 @@ var TEMPLATE_IDS = [
|
|
|
215
215
|
"oneline",
|
|
216
216
|
"rolodex"
|
|
217
217
|
];
|
|
218
|
+
var MULTI_COLUMN_TEMPLATES = [
|
|
219
|
+
"aurora",
|
|
220
|
+
"coral",
|
|
221
|
+
"prism",
|
|
222
|
+
"deedy",
|
|
223
|
+
"nova",
|
|
224
|
+
"axis",
|
|
225
|
+
"kintsugi",
|
|
226
|
+
"portrait",
|
|
227
|
+
"cameo",
|
|
228
|
+
"bauhaus",
|
|
229
|
+
"europass",
|
|
230
|
+
"rolodex"
|
|
231
|
+
];
|
|
232
|
+
function isMultiColumnTemplate(template) {
|
|
233
|
+
return !!template && MULTI_COLUMN_TEMPLATES.includes(template);
|
|
234
|
+
}
|
|
218
235
|
var Styling = z.object({
|
|
219
236
|
template: z.enum(TEMPLATE_IDS).default("aurora"),
|
|
220
237
|
colorMode: z.enum(["light", "dark"]).default("light"),
|
|
@@ -228,6 +245,11 @@ var Styling = z.object({
|
|
|
228
245
|
// em x 1000 (we treat as px-ish)
|
|
229
246
|
headingsLine: z.enum(["none", "underline", "left-bar", "boxed"]).default("none"),
|
|
230
247
|
spacing: z.enum(["compact", "regular", "spacious"]).default("regular"),
|
|
248
|
+
// Fine-grained, MS-Word-style spacing control (px @96dpi). Drive the gap
|
|
249
|
+
// between sections and between items within a section. Defaults match the
|
|
250
|
+
// previous hardcoded single-column values (mb-4 = 16px, ~10px item gaps).
|
|
251
|
+
sectionSpacing: z.number().default(16),
|
|
252
|
+
itemSpacing: z.number().default(10),
|
|
231
253
|
bulletGlyph: z.enum(["disc", "dash", "arrow", "square", "check", "none"]).default("disc"),
|
|
232
254
|
layout: z.enum(["single", "sidebar-left", "sidebar-right", "two-column"]).default("single"),
|
|
233
255
|
subtitlePlacement: z.enum(["below", "inline", "right"]).default("below"),
|
|
@@ -237,9 +259,9 @@ var Styling = z.object({
|
|
|
237
259
|
photoSize: z.number().default(72),
|
|
238
260
|
// Page margins in px @96dpi (used by the preview slicer + PDF export).
|
|
239
261
|
// Defaults match the previous hardcoded values: ~0.5in top/bottom, ~0.6in sides.
|
|
240
|
-
pageMarginX: z.number().default(
|
|
241
|
-
pageMarginTop: z.number().default(
|
|
242
|
-
pageMarginBottom: z.number().default(
|
|
262
|
+
pageMarginX: z.number().default(30),
|
|
263
|
+
pageMarginTop: z.number().default(30),
|
|
264
|
+
pageMarginBottom: z.number().default(30)
|
|
243
265
|
});
|
|
244
266
|
var Locale = z.object({
|
|
245
267
|
language: z.string().default("en-GB"),
|
|
@@ -1494,16 +1516,15 @@ function dobAgePenalty(resume) {
|
|
|
1494
1516
|
};
|
|
1495
1517
|
}
|
|
1496
1518
|
function singleColumnHard(resume) {
|
|
1497
|
-
const
|
|
1498
|
-
const twoCol = resume.sections.some((s) => s.columns === 2) || layout === "two-column" || layout === "sidebar-left" || layout === "sidebar-right";
|
|
1519
|
+
const twoCol = isMultiColumnTemplate(resume.styling?.template);
|
|
1499
1520
|
return {
|
|
1500
1521
|
dim: "ats",
|
|
1501
1522
|
check: hardCheck(
|
|
1502
1523
|
"AT12.single-column-hard",
|
|
1503
1524
|
"Single-column layout (ATS-critical)",
|
|
1504
1525
|
twoCol ? 0 : 1,
|
|
1505
|
-
twoCol ? "Two-column / sidebar
|
|
1506
|
-
"Switch to a single-column template (Jake, Harvard, Classic). Multi-column is
|
|
1526
|
+
twoCol ? "Two-column / sidebar template \u2014 a top cause of ATS parse failure. Workday can concatenate columns into gibberish. (The ATS-PDF export is always single-column, but the visual PDF is not.)" : "Single-column layout \u2014 every ATS parses this cleanly.",
|
|
1527
|
+
"Switch to a single-column template (Jake, Harvard, Classic) in the Templates tab. Multi-column is a top score-destroyer in ATS systems."
|
|
1507
1528
|
)
|
|
1508
1529
|
};
|
|
1509
1530
|
}
|
|
@@ -2519,7 +2540,7 @@ function atsChecks(resume, b) {
|
|
|
2519
2540
|
const s = resume.styling;
|
|
2520
2541
|
const p = resume.personal;
|
|
2521
2542
|
const allText = b.bullets.map((x) => x.text).join(" ");
|
|
2522
|
-
const isSingleCol = s.
|
|
2543
|
+
const isSingleCol = !isMultiColumnTemplate(s.template);
|
|
2523
2544
|
const isSafeFont = SAFE_FONTS.has(s.fontHeading) && SAFE_FONTS.has(s.fontBody);
|
|
2524
2545
|
const fontSizeOK = s.fontSize >= 9.5 && s.fontSize <= 12;
|
|
2525
2546
|
const safeBullet = SAFE_BULLET_GLYPHS.has(s.bulletGlyph === "disc" ? "\u2022" : s.bulletGlyph === "dash" ? "-" : "");
|
|
@@ -2558,7 +2579,7 @@ function atsChecks(resume, b) {
|
|
|
2558
2579
|
isSingleCol ? 1 : 0,
|
|
2559
2580
|
isSingleCol ? "Layout is single-column." : "Multi-column layout \u2014 many ATS parsers scramble reading order.",
|
|
2560
2581
|
[],
|
|
2561
|
-
!isSingleCol ? "
|
|
2582
|
+
!isSingleCol ? "Pick a single-column template (e.g. Jake, Harvard, Classic) in the Templates tab." : void 0,
|
|
2562
2583
|
!isSingleCol ? "hard" : "soft"
|
|
2563
2584
|
),
|
|
2564
2585
|
mk(
|
|
@@ -2894,10 +2915,38 @@ import path from "path";
|
|
|
2894
2915
|
var ROOT = path.resolve(process.env.JUICED_DATA_DIR || path.join(process.cwd(), "../..", "data"));
|
|
2895
2916
|
var RESUMES = path.join(ROOT, "resumes");
|
|
2896
2917
|
var VERSIONS = path.join(ROOT, "versions");
|
|
2918
|
+
var CLOUD_TOKEN = (process.env.JUICEDRESUME_TOKEN || "").trim();
|
|
2919
|
+
var CLOUD_URL = (process.env.JUICEDRESUME_URL || "https://juicedresume.com").replace(/\/+$/, "");
|
|
2920
|
+
var CLOUD = CLOUD_TOKEN.length > 0;
|
|
2897
2921
|
async function ensure(dir) {
|
|
2898
2922
|
await fs.mkdir(dir, { recursive: true });
|
|
2899
2923
|
}
|
|
2924
|
+
async function cloudReq(method, p, body) {
|
|
2925
|
+
let res;
|
|
2926
|
+
try {
|
|
2927
|
+
res = await fetch(`${CLOUD_URL}/api/mcp${p}`, {
|
|
2928
|
+
method,
|
|
2929
|
+
headers: {
|
|
2930
|
+
authorization: `Bearer ${CLOUD_TOKEN}`,
|
|
2931
|
+
...body !== void 0 ? { "content-type": "application/json" } : {}
|
|
2932
|
+
},
|
|
2933
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
2934
|
+
});
|
|
2935
|
+
} catch (e) {
|
|
2936
|
+
throw new Error(`JuicedResume: could not reach ${CLOUD_URL} (${e?.message || e}). Check JUICEDRESUME_URL / network.`);
|
|
2937
|
+
}
|
|
2938
|
+
if (res.status === 401) throw new Error("JuicedResume: invalid or missing JUICEDRESUME_TOKEN \u2014 rotate it in Settings \u2192 AI on juicedresume.com.");
|
|
2939
|
+
if (res.status === 402) throw new Error("JuicedResume: your plan does not include MCP/cloud access (Basic or Pro required).");
|
|
2940
|
+
if (res.status === 404) return null;
|
|
2941
|
+
if (!res.ok) {
|
|
2942
|
+
const detail = await res.text().catch(() => "");
|
|
2943
|
+
throw new Error(`JuicedResume cloud ${method} ${p} failed: ${res.status} ${detail}`);
|
|
2944
|
+
}
|
|
2945
|
+
const ct = res.headers.get("content-type") || "";
|
|
2946
|
+
return ct.includes("application/json") ? res.json() : null;
|
|
2947
|
+
}
|
|
2900
2948
|
async function listResumes() {
|
|
2949
|
+
if (CLOUD) return await cloudReq("GET", "/resumes") ?? [];
|
|
2901
2950
|
await ensure(RESUMES);
|
|
2902
2951
|
const files = await fs.readdir(RESUMES);
|
|
2903
2952
|
const out = [];
|
|
@@ -2912,6 +2961,10 @@ async function listResumes() {
|
|
|
2912
2961
|
return out.sort((a, b) => a.updatedAt < b.updatedAt ? 1 : -1);
|
|
2913
2962
|
}
|
|
2914
2963
|
async function getResume(id) {
|
|
2964
|
+
if (CLOUD) {
|
|
2965
|
+
const data = await cloudReq("GET", `/resumes/${id}`);
|
|
2966
|
+
return data ? Resume.parse(data) : null;
|
|
2967
|
+
}
|
|
2915
2968
|
try {
|
|
2916
2969
|
const raw = await fs.readFile(path.join(RESUMES, `${id}.json`), "utf8");
|
|
2917
2970
|
return Resume.parse(JSON.parse(raw));
|
|
@@ -2920,9 +2973,13 @@ async function getResume(id) {
|
|
|
2920
2973
|
}
|
|
2921
2974
|
}
|
|
2922
2975
|
async function saveResume(r) {
|
|
2976
|
+
const next = { ...r, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2977
|
+
if (CLOUD) {
|
|
2978
|
+
await cloudReq("PUT", `/resumes/${r.id}`, next);
|
|
2979
|
+
return next;
|
|
2980
|
+
}
|
|
2923
2981
|
await ensure(RESUMES);
|
|
2924
2982
|
await ensure(path.join(VERSIONS, r.id));
|
|
2925
|
-
const next = { ...r, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2926
2983
|
const file = path.join(RESUMES, `${r.id}.json`);
|
|
2927
2984
|
try {
|
|
2928
2985
|
const prev = await fs.readFile(file, "utf8");
|
|
@@ -2945,6 +3002,10 @@ async function createBlankResume(name = "Untitled") {
|
|
|
2945
3002
|
return await saveResume(blank);
|
|
2946
3003
|
}
|
|
2947
3004
|
async function deleteResume(id) {
|
|
3005
|
+
if (CLOUD) {
|
|
3006
|
+
await cloudReq("DELETE", `/resumes/${id}`);
|
|
3007
|
+
return;
|
|
3008
|
+
}
|
|
2948
3009
|
try {
|
|
2949
3010
|
await fs.unlink(path.join(RESUMES, `${id}.json`));
|
|
2950
3011
|
} catch {
|
|
@@ -3280,7 +3341,7 @@ function runOnce(cmd, args, cwd) {
|
|
|
3280
3341
|
import { promises as fs3 } from "fs";
|
|
3281
3342
|
import path3 from "path";
|
|
3282
3343
|
var server = new Server(
|
|
3283
|
-
{ name: "juiced-resume", version: "0.
|
|
3344
|
+
{ name: "juiced-resume", version: "0.3.1" },
|
|
3284
3345
|
{ capabilities: { tools: {} } }
|
|
3285
3346
|
);
|
|
3286
3347
|
var idArg = z2.object({ id: z2.string() });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juicedresume/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Model Context Protocol server for JuicedResume — score, tailor, and edit your resume from any MCP client (Claude Code, Claude Desktop, Cursor).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"@juiced/scoring": "*",
|
|
49
49
|
"@types/node": "^22.9.0",
|
|
50
50
|
"esbuild": "^0.24.2",
|
|
51
|
+
"esbuild-wasm": "^0.24.2",
|
|
51
52
|
"tsx": "^4.19.2",
|
|
52
53
|
"typescript": "^5.6.3"
|
|
53
54
|
},
|