@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.
Files changed (2) hide show
  1. package/dist/index.js +72 -11
  2. 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(56),
241
- pageMarginTop: z.number().default(48),
242
- pageMarginBottom: z.number().default(48)
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 layout = resume.styling?.layout;
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 layout detected \u2014 #1 cause of ATS parse failure. Workday concatenates columns into gibberish." : "Single-column layout \u2014 every ATS parses this cleanly.",
1506
- "Switch to a single-column template (Jake, Harvard, Classic). Multi-column is the top score-destroyer in ATS systems."
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.layout === "single";
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 ? "Switch to a single-column template (Customize \u2192 Layout \u2192 single)." : void 0,
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.2.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.2.2",
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
  },