careermate 0.1.2 → 0.1.3

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 (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -1
  3. package/apps/web/public/pages/documents.js +7 -2
  4. package/apps/web/public/pages/home.js +4 -7
  5. package/apps/web/public/pages/jobs.js +1 -5
  6. package/apps/web/public/pages/profile.js +3 -3
  7. package/apps/web/public/styles.css +7 -2
  8. package/dist/doctor.mjs +112 -90
  9. package/dist/install-page/analytics.js +97 -0
  10. package/dist/install-page/index.html +77 -56
  11. package/dist/install-page/og.png +0 -0
  12. package/dist/install-page/robots.txt +4 -0
  13. package/dist/install-page/sitemap.xml +21 -0
  14. package/dist/install-page/start.html +13 -10
  15. package/dist/install-page/style.css +75 -40
  16. package/dist/mcp.mjs +112 -90
  17. package/dist/migrate.mjs +95 -82
  18. package/dist/public/pages/documents.js +7 -2
  19. package/dist/public/pages/home.js +4 -7
  20. package/dist/public/pages/jobs.js +1 -5
  21. package/dist/public/pages/profile.js +3 -3
  22. package/dist/public/styles.css +7 -2
  23. package/dist/web.mjs +113 -86
  24. package/package.json +1 -2
  25. package/packages/db/src/repositories.ts +18 -1
  26. package/packages/mcp-tools/src/tools.ts +5 -5
  27. package/packages/parsers/src/documents.ts +13 -1
  28. package/packages/shared/src/schemas.ts +115 -81
  29. package/scripts/build-dist.mjs +7 -1
  30. package/scripts/gen-og.mjs +1 -1
  31. package/scripts/test.ts +25 -0
  32. package/scripts/ui-smoke.ts +32 -4
  33. package/dist/install-page/.omc/state/agent-replay-55a58c0d-bb6e-43ea-8a05-4b6c93b80b34.jsonl +0 -12
  34. package/dist/install-page/.omc/state/idle-notif-cooldown.json +0 -3
  35. package/dist/install-page/.omc/state/sessions/55a58c0d-bb6e-43ea-8a05-4b6c93b80b34/mission-state.json +0 -147
  36. package/dist/install-page/.omc/state/sessions/55a58c0d-bb6e-43ea-8a05-4b6c93b80b34/pre-tool-advisory-throttle.json +0 -22
  37. package/dist/install-page/.omc/state/sessions/55a58c0d-bb6e-43ea-8a05-4b6c93b80b34/subagent-tracking-state.json +0 -44
  38. package/docs/ARCHITECTURE.md +0 -208
  39. package/docs/CHANGES_V1.md +0 -103
  40. package/docs/DATA_MODEL.md +0 -460
  41. package/docs/DECISIONS.md +0 -277
  42. package/docs/DEMO.md +0 -242
  43. package/docs/MCP_TOOLS.md +0 -292
  44. package/docs/ROADMAP.md +0 -135
  45. package/docs/RUNTIME_DISTRIBUTION_RESEARCH.md +0 -135
  46. package/docs/START_WORKFLOW.md +0 -137
  47. package/docs/SUPPORTED_AI_APPS.md +0 -112
  48. package/docs/TODO.md +0 -57
  49. package/docs/UX_NOTES.md +0 -247
  50. package/docs/WORKFLOWS.md +0 -202
  51. package/docs/superpowers/specs/2026-06-16-demo-dashboard-deploy-design.md +0 -64
@@ -23,6 +23,7 @@ export async function render(ctx) {
23
23
  root.append(el('div', { class: 'tabs' }, tabCover, tabDocs), panel);
24
24
 
25
25
  let query = '';
26
+ let searchInput = null;
26
27
  function selectTab(next) {
27
28
  tab = next;
28
29
  query = '';
@@ -43,9 +44,12 @@ export async function render(ctx) {
43
44
  if (!hide) shown += 1;
44
45
  }
45
46
  const existing = panel.querySelector('.search-empty');
47
+ if (existing) existing.remove();
46
48
  if (q && cards.length && shown === 0) {
47
- if (!existing) panel.append(el('p', { class: 'search-empty muted', style: { textAlign: 'center', margin: '8px 0' } }, '검색 결과가 없어요.'));
48
- } else if (existing) existing.remove();
49
+ panel.append(el('div', { class: 'search-empty empty' },
50
+ el('p', { class: 'muted', style: { margin: '0 0 12px' } }, `‘${query.trim()}’에 맞는 항목이 없어요.`),
51
+ Btn('필터 초기화', { sm: true, variant: 'ghost', onClick: () => { query = ''; if (searchInput) searchInput.value = ''; applyFilter(); } })));
52
+ }
49
53
  }
50
54
 
51
55
  async function reload() {
@@ -70,6 +74,7 @@ export async function render(ctx) {
70
74
  function setActions() {
71
75
  const search = Input({ type: 'search', placeholder: tab === 'cover' ? '자기소개서 검색' : '문서 검색', value: query, attrs: { 'aria-label': '검색' } });
72
76
  search.classList.add('input--inline');
77
+ searchInput = search;
73
78
  search.addEventListener('input', () => { query = search.value; applyFilter(); });
74
79
  const createBtn = tab === 'cover'
75
80
  ? Btn('새 자기소개서', { icon: 'plus', variant: 'primary', onClick: () => openCoverCreate(reload) })
@@ -4,7 +4,7 @@
4
4
  // the 시작하기 card. Every empty state is ONE screen-specific sentence; never repeat
5
5
  // the same "AI에게 보내면 저장됩니다" instruction across surfaces.
6
6
  import {
7
- el, get, icon, navigate, Card, Badge, Btn, EmptyState, ListRow, CheckRow,
7
+ el, get, icon, navigate, Card, Badge, Btn, ListRow, CheckRow,
8
8
  Stat, PageHead, fmtDate, fmtRelative, scoreClass, STATUS_COLOR, statusColor, mount,
9
9
  } from '/lib.js';
10
10
 
@@ -59,7 +59,7 @@ function Greeting(name, s, firstRun) {
59
59
  let focus;
60
60
  if (firstRun) focus = '시작해 볼까요 — 먼저 기본 정보를 채워 주세요.';
61
61
  else if (s.interview_todo.length) focus = `면접 준비가 필요한 지원 ${s.interview_todo.length}건이 있어요.`;
62
- else if (s.in_progress.length) focus = `진행 중인 지원 ${s.in_progress.length}건을 확인하세요.`;
62
+ else if (s.in_progress.length) focus = `진행 중인 지원 ${s.in_progress.length}건이 있어요.`;
63
63
  else if (s.counts.jobs) focus = `저장된 공고 ${s.counts.jobs}건 · 새 적합도 분석을 받아보세요.`;
64
64
  else focus = '오늘도 한 걸음 나아가 볼까요.';
65
65
  return PageHead({ title: name ? `${name}님, 안녕하세요` : '환영합니다', desc: focus });
@@ -87,7 +87,7 @@ function GettingStarted(o) {
87
87
  el('div', { class: 'callout__body' },
88
88
  '공고 분석·자기소개서·면접 준비는 평소 쓰던 AI에게 말로 시키면, 결과가 이 대시보드에 쌓여요.'))),
89
89
  el('div', { class: 'progress' }, el('div', { class: 'progress__bar', style: { width: `${o.profile_completeness}%` } })),
90
- el('p', { class: 'text-secondary text-sm', style: { margin: '12px 0 8px' } },
90
+ el('p', { class: 'text-secondary text-sm mt-3 mb-2' },
91
91
  '완성할수록 AI의 적합도 분석과 자기소개서 품질이 좋아져요.'),
92
92
  el('div', {}, ...steps.map((it) => CheckRow({ done: it.done, label: it.label, onClick: () => navigate(it.to) }))),
93
93
  ],
@@ -201,10 +201,7 @@ function RecentJobs(jobs) {
201
201
  if (!jobs.length) {
202
202
  return Card({
203
203
  title: '최근 공고',
204
- body: EmptyState({
205
- iconName: 'briefcase', title: '저장된 공고가 아직 없어요',
206
- action: Btn('공고 추가', { icon: 'plus', variant: 'ghost', onClick: () => navigate('/jobs') }),
207
- }),
204
+ body: el('p', { class: 'muted', style: { margin: 0 } }, '저장된 공고가 아직 없어요. 새 공고를 추가해 보세요.'),
208
205
  });
209
206
  }
210
207
  const rows = jobs.map((j) => ListRow({
@@ -365,11 +365,7 @@ function FitCard(fit) {
365
365
  if (!fit) {
366
366
  return Card({
367
367
  title: '적합도 분석',
368
- body: EmptyState({
369
- iconName: 'target',
370
- title: '적합도 분석 전이에요',
371
- body: '이 공고와 내 프로필을 비교한 분석 결과가 여기에 표시됩니다.',
372
- }),
368
+ body: el('p', { class: 'muted', style: { margin: 0 } }, '아직 분석 전이에요. 이 공고와 내 프로필을 비교한 결과가 여기에 표시됩니다.'),
373
369
  });
374
370
  }
375
371
 
@@ -264,7 +264,7 @@ function editWriting(p, reload) {
264
264
  /* ---------------------------------------------------- 4. 경력 (Experiences) */
265
265
 
266
266
  function ExperiencesCard(items, reload) {
267
- const add = Btn('추가', { icon: 'plus', sm: true, variant: 'ghost', onClick: () => editExperience(null, reload) });
267
+ const add = Btn('경력 추가', { icon: 'plus', sm: true, variant: 'ghost', onClick: () => editExperience(null, reload) });
268
268
 
269
269
  if (!items.length) {
270
270
  return Card({
@@ -368,7 +368,7 @@ function editExperience(x, reload) {
368
368
  /* ----------------------------------------------------- 5. 프로젝트 (Projects) */
369
369
 
370
370
  function ProjectsCard(items, reload) {
371
- const add = Btn('추가', { icon: 'plus', sm: true, variant: 'ghost', onClick: () => editProject(null, reload) });
371
+ const add = Btn('프로젝트 추가', { icon: 'plus', sm: true, variant: 'ghost', onClick: () => editProject(null, reload) });
372
372
 
373
373
  if (!items.length) {
374
374
  return Card({
@@ -469,7 +469,7 @@ function editProject(x, reload) {
469
469
  /* -------------------------------------------------------- 6. 기술 (Skills) */
470
470
 
471
471
  function SkillsCard(items, reload) {
472
- const add = Btn('추가', { icon: 'plus', sm: true, variant: 'ghost', onClick: () => editSkill(null, reload) });
472
+ const add = Btn('기술 추가', { icon: 'plus', sm: true, variant: 'ghost', onClick: () => editSkill(null, reload) });
473
473
 
474
474
  if (!items.length) {
475
475
  return Card({
@@ -403,7 +403,9 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
403
403
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 22px 26px; max-height: 60vh; overflow-y: auto; }
404
404
  .kv { display: grid; grid-template-columns: 132px 1fr; gap: 8px 16px; }
405
405
  .kv dt { color: var(--text-tertiary); font-size: 13px; }
406
- .kv dd { margin: 0; }
406
+ /* min-width:0 overrides a grid item's default min-width:auto so the 1fr column
407
+ can shrink below its content — otherwise a long path/value overflows the grid. */
408
+ .kv dd { margin: 0; min-width: 0; }
407
409
 
408
410
  /* ----------------------------------------------------------------- modal */
409
411
  .modal-root { position: fixed; inset: 0; z-index: 60; display: none; }
@@ -450,7 +452,10 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
450
452
  .between { justify-content: space-between; } .center { align-items: center; } .wrap { flex-wrap: wrap; }
451
453
  .ml-auto { margin-left: auto; } .mt-2{margin-top:8px;} .mt-3{margin-top:12px;} .mt-4{margin-top:16px;} .mb-2{margin-bottom:8px;} .mb-3{margin-bottom:12px;} .mb-4{margin-bottom:16px;}
452
454
  .tnum { font-variant-numeric: tabular-nums; }
453
- .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
455
+ /* min-width:0 lets a truncating element actually shrink inside a flex row;
456
+ without it a long unbreakable value (e.g. a data-folder path) forces the
457
+ flex item to its content width and overflows the viewport on narrow screens. */
458
+ .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; min-width: 0; }
454
459
  .divider { height: 1px; background: var(--border); margin: 16px 0; }
455
460
  .skeleton { background: linear-gradient(90deg, var(--bg-sunken) 25%, var(--surface-hover) 37%, var(--bg-sunken) 63%); background-size: 400% 100%; animation: shimmer 1.4s infinite; border-radius: 6px; }
456
461
  @keyframes shimmer { 0% { background-position: 100% 0; } 100% { background-position: 0 0; } }
package/dist/web.mjs CHANGED
@@ -43892,29 +43892,42 @@ function isSafeUrl(u) {
43892
43892
  return !/^[a-z][a-z0-9+.-]*:/i.test(s) || /^(https?:|mailto:)/i.test(s);
43893
43893
  }
43894
43894
  var UNSAFE_URL_MSG = "javascript: \uAC19\uC740 \uC704\uD5D8\uD55C \uB9C1\uD06C\uB294 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. http(s) \uB9C1\uD06C\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.";
43895
- var SafeUrl = external_exports.string().min(1).refine(isSafeUrl, { message: UNSAFE_URL_MSG });
43896
- var SafeUrlOptional = external_exports.string().optional().refine((u) => u == null || u === "" || isSafeUrl(u), { message: UNSAFE_URL_MSG });
43895
+ var MAX_URL = 2e3;
43896
+ var SafeUrl = external_exports.string().min(1).max(MAX_URL).refine(isSafeUrl, { message: UNSAFE_URL_MSG });
43897
+ var SafeUrlOptional = external_exports.string().max(MAX_URL).optional().refine((u) => u == null || u === "" || isSafeUrl(u), { message: UNSAFE_URL_MSG });
43898
+ var MAX_LINE = 500;
43899
+ var MAX_NOTE = 1e4;
43900
+ var MAX_BODY = 2e5;
43901
+ var MAX_ITEM = 2e3;
43902
+ var MAX_ITEMS = 500;
43903
+ var EMPTY_MSG = "\uB0B4\uC6A9\uC774 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4(\uACF5\uBC31\uB9CC\uC73C\uB85C\uB294 \uC800\uC7A5\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4).";
43904
+ var reqLine = external_exports.string().trim().min(1).max(MAX_LINE);
43905
+ var optLine = external_exports.string().max(MAX_LINE).optional();
43906
+ var optNote = external_exports.string().max(MAX_NOTE).optional();
43907
+ var optBody = external_exports.string().max(MAX_BODY).optional();
43908
+ var reqBody = external_exports.string().max(MAX_BODY).refine((s) => s.trim().length > 0, { message: EMPTY_MSG });
43909
+ var strList = external_exports.array(external_exports.string().max(MAX_ITEM)).max(MAX_ITEMS);
43897
43910
  var baseRecord = {
43898
43911
  id,
43899
43912
  created_at: isoDateTime,
43900
43913
  updated_at: isoDateTime
43901
43914
  };
43902
43915
  var LinkSchema = external_exports.object({
43903
- label: external_exports.string().min(1),
43916
+ label: reqLine,
43904
43917
  url: SafeUrl
43905
43918
  });
43906
43919
  var ProfileInputSchema = external_exports.object({
43907
- name: external_exports.string().min(1).optional(),
43908
- email: external_exports.string().optional(),
43909
- phone: external_exports.string().optional(),
43910
- location: external_exports.string().optional(),
43911
- headline: external_exports.string().optional().describe("\uD55C \uC904 \uC18C\uAC1C / \uC9C1\uBB34 \uD0C0\uC774\uD2C0"),
43912
- summary: external_exports.string().optional().describe("\uC790\uAE30\uC18C\uAC1C \uC694\uC57D"),
43913
- desired_roles: external_exports.array(external_exports.string()).optional().describe("\uD76C\uB9DD \uC9C1\uBB34"),
43914
- desired_conditions: external_exports.string().optional().describe("\uD76C\uB9DD \uADFC\uBB34 \uC870\uAC74 (\uC5F0\uBD09, \uC9C0\uC5ED, \uADFC\uBB34\uD615\uD0DC \uB4F1)"),
43915
- preferred_tone: external_exports.string().optional().describe("\uC790\uAE30\uC18C\uAC1C\uC11C \uC120\uD638 \uBB38\uCCB4 (\uC608: \uB2F4\uBC31\uD558\uACE0 \uAD6C\uCCB4\uC801)"),
43916
- emphasis_points: external_exports.array(external_exports.string()).optional().describe("\uAC15\uC870\uD558\uACE0 \uC2F6\uC740 \uD575\uC2EC \uD3EC\uC778\uD2B8"),
43917
- links: external_exports.array(LinkSchema).optional().describe("\uD3EC\uD2B8\uD3F4\uB9AC\uC624/\uAE43\uD5C8\uBE0C/\uB9C1\uD06C\uB4DC\uC778 \uB4F1")
43920
+ name: reqLine.optional(),
43921
+ email: optLine,
43922
+ phone: optLine,
43923
+ location: optLine,
43924
+ headline: external_exports.string().max(MAX_LINE).optional().describe("\uD55C \uC904 \uC18C\uAC1C / \uC9C1\uBB34 \uD0C0\uC774\uD2C0"),
43925
+ summary: external_exports.string().max(MAX_NOTE).optional().describe("\uC790\uAE30\uC18C\uAC1C \uC694\uC57D"),
43926
+ desired_roles: strList.optional().describe("\uD76C\uB9DD \uC9C1\uBB34"),
43927
+ desired_conditions: external_exports.string().max(MAX_NOTE).optional().describe("\uD76C\uB9DD \uADFC\uBB34 \uC870\uAC74 (\uC5F0\uBD09, \uC9C0\uC5ED, \uADFC\uBB34\uD615\uD0DC \uB4F1)"),
43928
+ preferred_tone: external_exports.string().max(MAX_LINE).optional().describe("\uC790\uAE30\uC18C\uAC1C\uC11C \uC120\uD638 \uBB38\uCCB4 (\uC608: \uB2F4\uBC31\uD558\uACE0 \uAD6C\uCCB4\uC801)"),
43929
+ emphasis_points: strList.optional().describe("\uAC15\uC870\uD558\uACE0 \uC2F6\uC740 \uD575\uC2EC \uD3EC\uC778\uD2B8"),
43930
+ links: external_exports.array(LinkSchema).max(MAX_ITEMS).optional().describe("\uD3EC\uD2B8\uD3F4\uB9AC\uC624/\uAE43\uD5C8\uBE0C/\uB9C1\uD06C\uB4DC\uC778 \uB4F1")
43918
43931
  });
43919
43932
  var ProfileRecordSchema = external_exports.object({
43920
43933
  ...baseRecord,
@@ -43931,15 +43944,15 @@ var ProfileRecordSchema = external_exports.object({
43931
43944
  links: external_exports.array(LinkSchema)
43932
43945
  });
43933
43946
  var ExperienceInputSchema = external_exports.object({
43934
- company: external_exports.string().min(1),
43935
- role: external_exports.string().optional(),
43936
- employment_type: external_exports.string().optional().describe("\uC815\uADDC\uC9C1/\uACC4\uC57D\uC9C1/\uC778\uD134/\uD504\uB9AC\uB79C\uC11C \uB4F1"),
43937
- start_date: external_exports.string().optional().describe("YYYY-MM \uB610\uB294 YYYY-MM-DD"),
43938
- end_date: external_exports.string().optional().describe("\uC7AC\uC9C1 \uC911\uC774\uBA74 \uBE44\uC6CC\uB450\uAE30"),
43947
+ company: reqLine,
43948
+ role: optLine,
43949
+ employment_type: external_exports.string().max(MAX_LINE).optional().describe("\uC815\uADDC\uC9C1/\uACC4\uC57D\uC9C1/\uC778\uD134/\uD504\uB9AC\uB79C\uC11C \uB4F1"),
43950
+ start_date: external_exports.string().max(MAX_LINE).optional().describe("YYYY-MM \uB610\uB294 YYYY-MM-DD"),
43951
+ end_date: external_exports.string().max(MAX_LINE).optional().describe("\uC7AC\uC9C1 \uC911\uC774\uBA74 \uBE44\uC6CC\uB450\uAE30"),
43939
43952
  is_current: external_exports.boolean().optional(),
43940
- description: external_exports.string().optional(),
43941
- achievements: external_exports.array(external_exports.string()).optional().describe("\uC131\uACFC/\uC5C5\uC801 (\uC815\uB7C9 \uC9C0\uD45C \uAD8C\uC7A5)"),
43942
- tech: external_exports.array(external_exports.string()).optional().describe("\uC0AC\uC6A9 \uAE30\uC220/\uB3C4\uAD6C"),
43953
+ description: optBody,
43954
+ achievements: strList.optional().describe("\uC131\uACFC/\uC5C5\uC801 (\uC815\uB7C9 \uC9C0\uD45C \uAD8C\uC7A5)"),
43955
+ tech: strList.optional().describe("\uC0AC\uC6A9 \uAE30\uC220/\uB3C4\uAD6C"),
43943
43956
  order_index: external_exports.number().optional()
43944
43957
  });
43945
43958
  var ExperienceRecordSchema = external_exports.object({
@@ -43956,14 +43969,14 @@ var ExperienceRecordSchema = external_exports.object({
43956
43969
  order_index: external_exports.number()
43957
43970
  });
43958
43971
  var ProjectInputSchema = external_exports.object({
43959
- name: external_exports.string().min(1),
43960
- role: external_exports.string().optional(),
43961
- description: external_exports.string().optional(),
43962
- highlights: external_exports.array(external_exports.string()).optional(),
43963
- tech: external_exports.array(external_exports.string()).optional(),
43972
+ name: reqLine,
43973
+ role: optLine,
43974
+ description: optBody,
43975
+ highlights: strList.optional(),
43976
+ tech: strList.optional(),
43964
43977
  url: SafeUrlOptional,
43965
- start_date: external_exports.string().optional(),
43966
- end_date: external_exports.string().optional(),
43978
+ start_date: optLine,
43979
+ end_date: optLine,
43967
43980
  order_index: external_exports.number().optional()
43968
43981
  });
43969
43982
  var ProjectRecordSchema = external_exports.object({
@@ -43979,9 +43992,9 @@ var ProjectRecordSchema = external_exports.object({
43979
43992
  order_index: external_exports.number()
43980
43993
  });
43981
43994
  var SkillInputSchema = external_exports.object({
43982
- name: external_exports.string().min(1),
43983
- category: external_exports.string().optional().describe("\uC5B8\uC5B4/\uD504\uB808\uC784\uC6CC\uD06C/\uD234/\uC18C\uD504\uD2B8\uC2A4\uD0AC \uB4F1"),
43984
- level: external_exports.string().optional().describe("\uC0C1/\uC911/\uD558 \uB610\uB294 \uC790\uC720 \uC11C\uC220"),
43995
+ name: reqLine,
43996
+ category: external_exports.string().max(MAX_LINE).optional().describe("\uC5B8\uC5B4/\uD504\uB808\uC784\uC6CC\uD06C/\uD234/\uC18C\uD504\uD2B8\uC2A4\uD0AC \uB4F1"),
43997
+ level: external_exports.string().max(MAX_LINE).optional().describe("\uC0C1/\uC911/\uD558 \uB610\uB294 \uC790\uC720 \uC11C\uC220"),
43985
43998
  years: external_exports.number().optional(),
43986
43999
  order_index: external_exports.number().optional()
43987
44000
  });
@@ -43995,11 +44008,11 @@ var SkillRecordSchema = external_exports.object({
43995
44008
  });
43996
44009
  var DocumentInputSchema = external_exports.object({
43997
44010
  kind: external_exports.enum(DOCUMENT_KINDS),
43998
- title: external_exports.string().min(1),
43999
- content: external_exports.string().describe("Markdown \uB610\uB294 \uC77C\uBC18 \uD14D\uC2A4\uD2B8 \uBCF8\uBB38"),
44011
+ title: reqLine,
44012
+ content: reqBody.describe("Markdown \uB610\uB294 \uC77C\uBC18 \uD14D\uC2A4\uD2B8 \uBCF8\uBB38"),
44000
44013
  source: external_exports.enum(CONTENT_SOURCES).optional(),
44001
44014
  is_primary: external_exports.boolean().optional().describe("\uB300\uD45C \uBB38\uC11C \uC5EC\uBD80"),
44002
- tags: external_exports.array(external_exports.string()).optional()
44015
+ tags: strList.optional()
44003
44016
  });
44004
44017
  var DocumentRecordSchema = external_exports.object({
44005
44018
  ...baseRecord,
@@ -44011,19 +44024,19 @@ var DocumentRecordSchema = external_exports.object({
44011
44024
  tags: external_exports.array(external_exports.string())
44012
44025
  });
44013
44026
  var CoverLetterInputSchema = external_exports.object({
44014
- title: external_exports.string().min(1),
44015
- job_id: external_exports.string().optional().describe("\uD2B9\uC815 \uACF5\uACE0\uC5D0 \uC5F0\uACB0\uD560 \uACBD\uC6B0"),
44027
+ title: reqLine,
44028
+ job_id: optLine.describe("\uD2B9\uC815 \uACF5\uACE0\uC5D0 \uC5F0\uACB0\uD560 \uACBD\uC6B0"),
44016
44029
  is_primary: external_exports.boolean().optional(),
44017
- content: external_exports.string().optional().describe("\uCD08\uAE30 \uBC84\uC804 \uBCF8\uBB38 (\uC788\uC73C\uBA74 v1\uB85C \uC800\uC7A5)"),
44030
+ content: optBody.describe("\uCD08\uAE30 \uBC84\uC804 \uBCF8\uBB38 (\uC788\uC73C\uBA74 v1\uB85C \uC800\uC7A5)"),
44018
44031
  source: external_exports.enum(CONTENT_SOURCES).optional(),
44019
- note: external_exports.string().optional().describe("\uCD08\uAE30 \uBC84\uC804 \uBA54\uBAA8")
44032
+ note: optNote.describe("\uCD08\uAE30 \uBC84\uC804 \uBA54\uBAA8")
44020
44033
  });
44021
44034
  var CoverLetterVersionInputSchema = external_exports.object({
44022
- cover_letter_id: external_exports.string().optional().describe("\uAE30\uC874 \uC790\uAE30\uC18C\uAC1C\uC11C\uC5D0 \uC0C8 \uBC84\uC804 \uCD94\uAC00 \uC2DC"),
44023
- title: external_exports.string().optional().describe("\uC0C8 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uB9CC\uB4E4 \uB54C \uC81C\uBAA9"),
44024
- job_id: external_exports.string().optional(),
44025
- content: external_exports.string().min(1),
44026
- note: external_exports.string().optional().describe("\uC774 \uBC84\uC804\uC758 \uBCC0\uACBD \uC694\uC57D (\uC608: \uC9C0\uC6D0\uB3D9\uAE30 \uBCF4\uAC15)"),
44035
+ cover_letter_id: optLine.describe("\uAE30\uC874 \uC790\uAE30\uC18C\uAC1C\uC11C\uC5D0 \uC0C8 \uBC84\uC804 \uCD94\uAC00 \uC2DC"),
44036
+ title: external_exports.string().max(MAX_LINE).optional().describe("\uC0C8 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uB9CC\uB4E4 \uB54C \uC81C\uBAA9"),
44037
+ job_id: optLine,
44038
+ content: reqBody,
44039
+ note: optNote.describe("\uC774 \uBC84\uC804\uC758 \uBCC0\uACBD \uC694\uC57D (\uC608: \uC9C0\uC6D0\uB3D9\uAE30 \uBCF4\uAC15)"),
44027
44040
  source: external_exports.enum(CONTENT_SOURCES).optional(),
44028
44041
  set_current: external_exports.boolean().optional().describe("\uC774 \uBC84\uC804\uC744 \uD604\uC7AC \uBC84\uC804\uC73C\uB85C \uC9C0\uC815 (\uAE30\uBCF8 true)")
44029
44042
  });
@@ -44047,16 +44060,16 @@ var CoverLetterRecordSchema = external_exports.object({
44047
44060
  versions: external_exports.array(CoverLetterVersionRecordSchema).optional()
44048
44061
  });
44049
44062
  var JobInputSchema = external_exports.object({
44050
- company: external_exports.string().min(1),
44051
- position: external_exports.string().min(1).describe("\uC9C1\uBB34/\uD3EC\uC9C0\uC158\uBA85"),
44063
+ company: reqLine,
44064
+ position: reqLine.describe("\uC9C1\uBB34/\uD3EC\uC9C0\uC158\uBA85"),
44052
44065
  url: SafeUrlOptional,
44053
- location: external_exports.string().optional(),
44054
- employment_type: external_exports.string().optional(),
44055
- description: external_exports.string().optional().describe("\uACF5\uACE0 \uC6D0\uBB38 \uB610\uB294 \uC815\uB9AC\uB41C \uD14D\uC2A4\uD2B8"),
44056
- requirements: external_exports.array(external_exports.string()).optional().describe("\uC790\uACA9\uC694\uAC74/\uC6B0\uB300\uC0AC\uD56D \uD575\uC2EC"),
44057
- keywords: external_exports.array(external_exports.string()).optional().describe("\uD575\uC2EC \uD0A4\uC6CC\uB4DC"),
44058
- deadline: external_exports.string().optional().describe("\uB9C8\uAC10\uC77C YYYY-MM-DD"),
44059
- source: external_exports.string().optional().describe("\uCD9C\uCC98 (\uC0AC\uB78C\uC778/\uC6D0\uD2F0\uB4DC/\uC9C1\uC811 \uC785\uB825 \uB4F1)")
44066
+ location: optLine,
44067
+ employment_type: optLine,
44068
+ description: optBody.describe("\uACF5\uACE0 \uC6D0\uBB38 \uB610\uB294 \uC815\uB9AC\uB41C \uD14D\uC2A4\uD2B8"),
44069
+ requirements: strList.optional().describe("\uC790\uACA9\uC694\uAC74/\uC6B0\uB300\uC0AC\uD56D \uD575\uC2EC"),
44070
+ keywords: strList.optional().describe("\uD575\uC2EC \uD0A4\uC6CC\uB4DC"),
44071
+ deadline: external_exports.string().max(MAX_LINE).optional().describe("\uB9C8\uAC10\uC77C YYYY-MM-DD"),
44072
+ source: external_exports.string().max(MAX_LINE).optional().describe("\uCD9C\uCC98 (\uC0AC\uB78C\uC778/\uC6D0\uD2F0\uB4DC/\uC9C1\uC811 \uC785\uB825 \uB4F1)")
44060
44073
  });
44061
44074
  var JobRecordSchema = external_exports.object({
44062
44075
  ...baseRecord,
@@ -44072,14 +44085,14 @@ var JobRecordSchema = external_exports.object({
44072
44085
  source: external_exports.string().nullable()
44073
44086
  });
44074
44087
  var FitAnalysisInputSchema = external_exports.object({
44075
- job_id: external_exports.string().describe("\uBD84\uC11D \uB300\uC0C1 \uACF5\uACE0 ID"),
44088
+ job_id: reqLine.describe("\uBD84\uC11D \uB300\uC0C1 \uACF5\uACE0 ID"),
44076
44089
  score: external_exports.number().min(0).max(100).optional().describe("\uC885\uD569 \uC801\uD569\uB3C4 0~100"),
44077
- summary: external_exports.string().optional().describe("\uD55C\uB450 \uBB38\uB2E8 \uC694\uC57D"),
44078
- strengths: external_exports.array(external_exports.string()).optional().describe("\uAC15\uC810 / \uC798 \uB9DE\uB294 \uBD80\uBD84"),
44079
- gaps: external_exports.array(external_exports.string()).optional().describe("\uBD80\uC871\uD55C \uBD80\uBD84 / \uBCF4\uC644 \uD544\uC694"),
44080
- matched_keywords: external_exports.array(external_exports.string()).optional(),
44081
- missing_keywords: external_exports.array(external_exports.string()).optional(),
44082
- recommendations: external_exports.array(external_exports.string()).optional().describe("\uC790\uAE30\uC18C\uAC1C\uC11C/\uC9C0\uC6D0 \uC804\uB7B5 \uC81C\uC548")
44090
+ summary: external_exports.string().max(MAX_NOTE).optional().describe("\uD55C\uB450 \uBB38\uB2E8 \uC694\uC57D"),
44091
+ strengths: strList.optional().describe("\uAC15\uC810 / \uC798 \uB9DE\uB294 \uBD80\uBD84"),
44092
+ gaps: strList.optional().describe("\uBD80\uC871\uD55C \uBD80\uBD84 / \uBCF4\uC644 \uD544\uC694"),
44093
+ matched_keywords: strList.optional(),
44094
+ missing_keywords: strList.optional(),
44095
+ recommendations: strList.optional().describe("\uC790\uAE30\uC18C\uAC1C\uC11C/\uC9C0\uC6D0 \uC804\uB7B5 \uC81C\uC548")
44083
44096
  });
44084
44097
  var FitAnalysisRecordSchema = external_exports.object({
44085
44098
  ...baseRecord,
@@ -44093,18 +44106,18 @@ var FitAnalysisRecordSchema = external_exports.object({
44093
44106
  recommendations: external_exports.array(external_exports.string())
44094
44107
  });
44095
44108
  var ApplicationInputSchema = external_exports.object({
44096
- job_id: external_exports.string(),
44109
+ job_id: reqLine,
44097
44110
  status: external_exports.enum(APPLICATION_STATUSES).optional(),
44098
- resume_id: external_exports.string().optional(),
44099
- cover_letter_id: external_exports.string().optional(),
44100
- applied_at: external_exports.string().optional(),
44101
- notes: external_exports.string().optional()
44111
+ resume_id: optLine,
44112
+ cover_letter_id: optLine,
44113
+ applied_at: optLine,
44114
+ notes: optNote
44102
44115
  });
44103
44116
  var ApplicationStatusUpdateSchema = external_exports.object({
44104
- job_id: external_exports.string().optional(),
44105
- application_id: external_exports.string().optional(),
44117
+ job_id: optLine,
44118
+ application_id: optLine,
44106
44119
  status: external_exports.enum(APPLICATION_STATUSES),
44107
- note: external_exports.string().optional().describe("\uC0C1\uD0DC \uBCC0\uACBD \uC0AC\uC720/\uBA54\uBAA8")
44120
+ note: optNote.describe("\uC0C1\uD0DC \uBCC0\uACBD \uC0AC\uC720/\uBA54\uBAA8")
44108
44121
  });
44109
44122
  var ApplicationRecordSchema = external_exports.object({
44110
44123
  ...baseRecord,
@@ -44116,24 +44129,24 @@ var ApplicationRecordSchema = external_exports.object({
44116
44129
  notes: external_exports.string().nullable()
44117
44130
  });
44118
44131
  var StarGuideSchema = external_exports.object({
44119
- question: external_exports.string(),
44120
- situation: external_exports.string().optional(),
44121
- task: external_exports.string().optional(),
44122
- action: external_exports.string().optional(),
44123
- result: external_exports.string().optional()
44132
+ question: external_exports.string().max(MAX_NOTE),
44133
+ situation: optNote,
44134
+ task: optNote,
44135
+ action: optNote,
44136
+ result: optNote
44124
44137
  });
44125
44138
  var InterviewQuestionSchema = external_exports.object({
44126
- question: external_exports.string(),
44127
- intent: external_exports.string().optional().describe("\uC9C8\uBB38 \uC758\uB3C4"),
44128
- followups: external_exports.array(external_exports.string()).optional().describe("\uC608\uC0C1 \uAF2C\uB9AC \uC9C8\uBB38"),
44129
- answer_outline: external_exports.string().optional().describe("\uB2F5\uBCC0 \uAC00\uC774\uB4DC/\uD575\uC2EC \uD3EC\uC778\uD2B8")
44139
+ question: external_exports.string().max(MAX_NOTE),
44140
+ intent: external_exports.string().max(MAX_NOTE).optional().describe("\uC9C8\uBB38 \uC758\uB3C4"),
44141
+ followups: strList.optional().describe("\uC608\uC0C1 \uAF2C\uB9AC \uC9C8\uBB38"),
44142
+ answer_outline: optBody.describe("\uB2F5\uBCC0 \uAC00\uC774\uB4DC/\uD575\uC2EC \uD3EC\uC778\uD2B8")
44130
44143
  });
44131
44144
  var InterviewPrepInputSchema = external_exports.object({
44132
- job_id: external_exports.string(),
44133
- questions: external_exports.array(InterviewQuestionSchema).optional(),
44134
- star_guides: external_exports.array(StarGuideSchema).optional(),
44135
- self_introduction: external_exports.string().optional().describe("1\uBD84 \uC790\uAE30\uC18C\uAC1C \uCD08\uC548"),
44136
- notes: external_exports.string().optional().describe("\uBA74\uC811 \uD6C4\uAE30/\uBA54\uBAA8")
44145
+ job_id: reqLine,
44146
+ questions: external_exports.array(InterviewQuestionSchema).max(MAX_ITEMS).optional(),
44147
+ star_guides: external_exports.array(StarGuideSchema).max(MAX_ITEMS).optional(),
44148
+ self_introduction: optBody.describe("1\uBD84 \uC790\uAE30\uC18C\uAC1C \uCD08\uC548"),
44149
+ notes: optNote.describe("\uBA74\uC811 \uD6C4\uAE30/\uBA54\uBAA8")
44137
44150
  });
44138
44151
  var InterviewPrepRecordSchema = external_exports.object({
44139
44152
  ...baseRecord,
@@ -44187,7 +44200,7 @@ var ApplicationContextSchema = external_exports.object({
44187
44200
  });
44188
44201
 
44189
44202
  // packages/shared/src/build-info.ts
44190
- var APP_VERSION = true ? "0.1.2" : "0.0.0-dev";
44203
+ var APP_VERSION = true ? "0.1.3" : "0.0.0-dev";
44191
44204
  var BUNDLED = true;
44192
44205
 
44193
44206
  // packages/shared/src/index.ts
@@ -44903,7 +44916,21 @@ var activityRepo = {
44903
44916
  };
44904
44917
  function getEntityCounts() {
44905
44918
  const db = getDb();
44906
- const c = (t) => db.prepare(`SELECT COUNT(*) AS n FROM ${t}`).get().n;
44919
+ const ALLOWED_TABLES = /* @__PURE__ */ new Set([
44920
+ "profile",
44921
+ "experiences",
44922
+ "projects",
44923
+ "skills",
44924
+ "documents",
44925
+ "cover_letters",
44926
+ "jobs",
44927
+ "applications",
44928
+ "interview_preps"
44929
+ ]);
44930
+ const c = (t) => {
44931
+ if (!ALLOWED_TABLES.has(t)) throw new Error(`getEntityCounts: \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uC740 \uD14C\uC774\uBE14 \uC774\uB984: ${t}`);
44932
+ return db.prepare(`SELECT COUNT(*) AS n FROM ${t}`).get().n;
44933
+ };
44907
44934
  return {
44908
44935
  profile: c("profile"),
44909
44936
  experiences: c("experiences"),
@@ -44979,7 +45006,7 @@ function injectToken(html) {
44979
45006
  // apps/web/src/http.ts
44980
45007
  import fs3 from "node:fs";
44981
45008
  import path3 from "node:path";
44982
- var MAX_BODY = 8 * 1024 * 1024;
45009
+ var MAX_BODY2 = 8 * 1024 * 1024;
44983
45010
  var Router = class {
44984
45011
  routes = [];
44985
45012
  add(method, path6, handler) {
@@ -45074,7 +45101,7 @@ function readBody(req) {
45074
45101
  const chunks = [];
45075
45102
  req.on("data", (c) => {
45076
45103
  size2 += c.length;
45077
- if (size2 > MAX_BODY) {
45104
+ if (size2 > MAX_BODY2) {
45078
45105
  reject2(new HttpError(413, "\uC694\uCCAD \uBCF8\uBB38\uC774 \uB108\uBB34 \uD07D\uB2C8\uB2E4.", "too_large"));
45079
45106
  req.destroy();
45080
45107
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "careermate",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "description": "CareerMate — an MCP-first, local-first career workspace. Connect it to a local AI agent (Claude Desktop, Claude Code, Codex); CareerMate stores your career data locally on your machine and exposes it over MCP.",
6
6
  "license": "MIT",
@@ -29,7 +29,6 @@
29
29
  "apps",
30
30
  "packages",
31
31
  "scripts",
32
- "docs",
33
32
  "tsconfig.json",
34
33
  "README.md",
35
34
  "THIRD_PARTY_NOTICES.md"
@@ -859,7 +859,24 @@ export const activityRepo = {
859
859
  */
860
860
  export function getEntityCounts(): Record<string, number> {
861
861
  const db = getDb();
862
- const c = (t: string): number => (db.prepare(`SELECT COUNT(*) AS n FROM ${t}`).get() as { n: number }).n;
862
+ // Table names are hardcoded below, but the name is interpolated into SQL, so
863
+ // guard with an allowlist as defense-in-depth: a future caller can never turn
864
+ // this into an injection sink even if `t` were sourced from elsewhere.
865
+ const ALLOWED_TABLES = new Set([
866
+ 'profile',
867
+ 'experiences',
868
+ 'projects',
869
+ 'skills',
870
+ 'documents',
871
+ 'cover_letters',
872
+ 'jobs',
873
+ 'applications',
874
+ 'interview_preps',
875
+ ]);
876
+ const c = (t: string): number => {
877
+ if (!ALLOWED_TABLES.has(t)) throw new Error(`getEntityCounts: 허용되지 않은 테이블 이름: ${t}`);
878
+ return (db.prepare(`SELECT COUNT(*) AS n FROM ${t}`).get() as { n: number }).n;
879
+ };
863
880
  return {
864
881
  profile: c('profile'),
865
882
  experiences: c('experiences'),
@@ -22,6 +22,7 @@ import {
22
22
  FitAnalysisInputSchema,
23
23
  CoverLetterVersionInputSchema,
24
24
  InterviewPrepInputSchema,
25
+ DocumentInputSchema,
25
26
  APPLICATION_STATUSES,
26
27
  APPLICATION_STATUS_LABELS,
27
28
  DOCUMENT_KINDS,
@@ -272,13 +273,12 @@ export const TOOLS: ToolDef[] = [
272
273
  title: '이력서/경력기술서 추가',
273
274
  description:
274
275
  '이력서, 경력기술서, 포트폴리오 등 문서를 저장합니다. content에는 사용자가 업로드/붙여넣은 텍스트(또는 정리한 Markdown)를 넣으세요. kind 기본값은 resume입니다. is_primary=true로 대표 문서를 지정하면 get_application_context의 primary_resume로 노출됩니다.',
276
+ // Reuse the shared (length-bounded, whitespace-rejecting) Document schema so
277
+ // the MCP ingestion path — the primary route for résumé text — inherits the
278
+ // same caps as the web API. kind stays optional here (defaults to resume).
275
279
  inputSchema: {
276
- title: z.string().describe('문서 제목'),
277
- content: z.string().describe('본문 (Markdown/일반 텍스트)'),
280
+ ...DocumentInputSchema.omit({ kind: true }).shape,
278
281
  kind: z.enum(DOCUMENT_KINDS).optional().describe('resume(이력서)/career_description(경력기술서)/portfolio/other'),
279
- source: z.enum(CONTENT_SOURCES).optional(),
280
- is_primary: z.boolean().optional().describe('대표 문서 여부'),
281
- tags: z.array(z.string()).optional(),
282
282
  },
283
283
  handler: (args) => {
284
284
  const doc = addResume(args);
@@ -427,9 +427,21 @@ async function finishPdf(buf: Buffer, base: BaseFn): Promise<ExtractDocumentResu
427
427
  });
428
428
  return base('pdf', { text, bytes: buf.length, warnings: pageWarnings });
429
429
  } catch (e) {
430
+ // pdfjs throws a named PasswordException for password-protected PDFs — give
431
+ // a distinct, actionable hint instead of the generic "corrupted or encrypted".
432
+ const name = e && typeof e === 'object' && 'name' in e ? String((e as { name?: unknown }).name) : '';
433
+ if (name === 'PasswordException') {
434
+ return base('unsupported', {
435
+ bytes: buf.length,
436
+ warnings: [
437
+ '이 PDF는 암호(비밀번호)로 보호되어 있어 읽을 수 없습니다. PDF 뷰어에서 암호를 해제해 다시 저장하거나, 내용을 텍스트로 붙여넣어 주세요.',
438
+ ],
439
+ unsupported: true,
440
+ });
441
+ }
430
442
  return base('unsupported', {
431
443
  bytes: buf.length,
432
- warnings: [`PDF를 읽지 못했습니다(${errMsg(e)}). 암호화되었거나 손상된 PDF일 수 있습니다. 텍스트로 붙여넣어 주세요.`],
444
+ warnings: [`PDF를 읽지 못했습니다(${errMsg(e)}). 손상되었거나 지원하지 않는 PDF일 수 있습니다. 텍스트로 붙여넣어 주세요.`],
433
445
  unsupported: true,
434
446
  });
435
447
  }