careermate 0.0.9 → 0.0.12

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 (52) hide show
  1. package/AGENTS.md +3 -2
  2. package/INSTALL.md +6 -6
  3. package/LICENSE +1 -1
  4. package/README.md +18 -16
  5. package/dist/career-os/README.md +2 -2
  6. package/dist/doctor.mjs +17857 -6595
  7. package/dist/mcp.mjs +14545 -3839
  8. package/dist/migrate.mjs +14258 -3709
  9. package/dist/pdf.worker.min.mjs +4 -4
  10. package/dist/public/app.js +3 -18
  11. package/dist/public/i18n/en/documents.js +1 -0
  12. package/dist/public/i18n/en/home.js +8 -1
  13. package/dist/public/i18n/en/jobs.js +2 -0
  14. package/dist/public/i18n/en/nav.js +1 -0
  15. package/dist/public/i18n/en/settings.js +1 -6
  16. package/dist/public/i18n/ko/documents.js +1 -0
  17. package/dist/public/i18n/ko/home.js +8 -1
  18. package/dist/public/i18n/ko/jobs.js +2 -0
  19. package/dist/public/i18n/ko/nav.js +1 -0
  20. package/dist/public/i18n/ko/settings.js +1 -6
  21. package/dist/public/index.html +0 -1
  22. package/dist/public/lib.js +13 -4
  23. package/dist/public/pages/documents.js +27 -10
  24. package/dist/public/pages/home.js +15 -5
  25. package/dist/public/pages/insights.js +156 -0
  26. package/dist/public/pages/jobs.js +21 -3
  27. package/dist/public/pages/settings.js +13 -12
  28. package/dist/public/styles.css +33 -0
  29. package/dist/web.mjs +16890 -5492
  30. package/docs/FAQ.md +3 -4
  31. package/docs/LOCAL_FIRST.md +32 -0
  32. package/docs/MCP_TOOLS.md +6 -3
  33. package/docs/REVIEW_IMPLEMENTATION_PLAN.md +22 -0
  34. package/docs/career-os/README.md +2 -2
  35. package/package.json +7 -12
  36. package/packages/core/src/data-health.ts +161 -0
  37. package/packages/core/src/fit-explain.ts +63 -0
  38. package/packages/core/src/index.ts +4 -0
  39. package/packages/core/src/job-categories.data.ts +140 -0
  40. package/packages/core/src/job-categories.ts +106 -0
  41. package/packages/core/src/jobsearch.ts +374 -66
  42. package/packages/core/src/reuse-insights.ts +126 -0
  43. package/packages/db/src/connection.ts +46 -26
  44. package/packages/db/src/index.ts +3 -0
  45. package/packages/db/src/interview-retrospectives.ts +145 -0
  46. package/packages/db/src/job-search.ts +100 -0
  47. package/packages/db/src/provenance.ts +125 -0
  48. package/packages/db/src/repositories.ts +1 -0
  49. package/packages/mcp-tools/src/tools.ts +115 -11
  50. package/packages/prompts/src/system.ts +21 -64
  51. package/scripts/init.ts +2 -0
  52. package/tsconfig.json +9 -10
@@ -48,6 +48,8 @@ import {
48
48
  getInboxDir,
49
49
  getVerifyStrict,
50
50
  setVerifyStrict,
51
+ getJobSearchPreferences,
52
+ setJobSearchPreferences,
51
53
  } from '@careermate/db';
52
54
  import {
53
55
  getOnboardingStatus,
@@ -68,6 +70,9 @@ import {
68
70
  prescreenJob,
69
71
  searchJobs,
70
72
  JOB_SEARCH_SOURCES,
73
+ findJobCategories,
74
+ isRememberCategory,
75
+ isSaraminCategory,
71
76
  saveOffer,
72
77
  compareOffers,
73
78
  getOffer,
@@ -110,6 +115,12 @@ const STATUS_LIST = APPLICATION_STATUSES.map((s) => `${s}(${APPLICATION_STATUS_L
110
115
  const DELETE_CONFIRMATION = 'DELETE';
111
116
  const DASHBOARD_URL = 'http://127.0.0.1:4319';
112
117
 
118
+ /** A real calendar day in YYYY-MM-DD (rejects 2026-99-99 and 2026-02-30). */
119
+ const isYmd = (s: string) => {
120
+ const d = new Date(`${s}T00:00:00Z`);
121
+ return /^\d{4}-\d{2}-\d{2}$/.test(s) && !Number.isNaN(d.getTime()) && d.toISOString().startsWith(s);
122
+ };
123
+
113
124
  /** List real, user-dropped files in the inbox (skips hidden/OS/office-lock cruft). */
114
125
  function listInboxFiles(dir: string): { name: string; path: string }[] {
115
126
  let names: string[];
@@ -1080,26 +1091,119 @@ export const TOOLS: ToolDef[] = [
1080
1091
  },
1081
1092
  {
1082
1093
  name: 'search_jobs',
1083
- // 외부 공개 잡보드(원티드·점핏)로 검색어가 나간다 — openWorld를 정직하게 표시.
1094
+ // 외부 공개 잡보드(원티드·점핏·리멤버·사람인)로 검색어·직무 조건이 나가므로 openWorld를 정직하게 표시.
1084
1095
  openWorld: true,
1085
- title: '공고 검색·가져오기(키워드)',
1096
+ title: '공고 검색·가져오기(키워드·직무·신규)',
1086
1097
  description:
1087
- `키워드로 무인증 공개 채용 사이트(${JOB_SEARCH_SOURCES.join('·')})에서 채용 공고를 가져옵니다. 사용자가 "백엔드 신입 공고 찾아줘", "React 공고 가져와"처럼 공고를 찾아 달라고 할 때 사용하세요. 결과(회사·직무·링크·지역·경력·기술스택·마감)를 사용자에게 보기 좋게 보여 주고, **사용자가 관심 있다고 고른 것만** save_job_posting으로 저장하세요(전부 자동 저장하지 마세요). 저장한 공고는 이어서 get_application_context로 분석·자소서로 연결됩니다. ` +
1088
- `참고: 이 도구는 외부 공개 페이지에서 공고를 가져오기만 하며(보내는 것은 검색 키워드뿐, 사용자 데이터는 절대 전송하지 않음), 봇 차단 우회는 하지 않습니다. 사람인·잡코리아는 키 발급 또는 약관 제약으로 여기 포함되지 않습니다. 결과가 비어 있으면 키워드를 바꾸거나 사용자에게 공고 URL/본문을 받아 save_job_posting으로 저장하세요.`,
1098
+ `키워드나 직무 조건으로 무인증 공개 채용 사이트(${JOB_SEARCH_SOURCES.join('·')})에서 채용 공고를 가져옵니다. 사용자가 "영업 신입 공고 찾아줘", "React 공고 가져와", "오늘 올라온 내 직무 공고 보여줘"처럼 공고를 찾아 달라고 할 때 사용하세요. 결과(회사·직무·링크·지역·경력·기술·마감·게시일)를 사용자에게 보기 좋게 보여 주고, **사용자가 관심 있다고 고른 것만** save_job_posting으로 저장하세요(전부 자동 저장하지 마세요). 저장한 공고는 이어서 get_application_context로 분석·자소서로 연결됩니다. ` +
1099
+ `직무 조건: 모든 직군(영업·회계·디자인·의료·생산 등)에 쓸 수 있습니다. 값은 짐작하지 말고 find_job_categories로 사용자 직무에 맞는 리멤버 분류("상위>하위")와 사람인 코드를 찾아 넣으세요. keyword와 직무 조건을 모두 비우면 set_job_search_preferences로 저장해 둔 조건을 씁니다(저장된 게 없으면 먼저 저장을 제안하세요). 직무 조건만 있으면 조건을 받은 사이트(리멤버·사람인)만 검색합니다. ` +
1100
+ `새 공고: posted_since("today" 또는 YYYY-MM-DD)를 주면 그날 이후 공고만 남깁니다. 리멤버·사람인은 사이트 게시일로, 날짜를 주지 않는 원티드·점핏은 CareerMate가 처음 본 날(first_seen_at)로 거르므로, 원티드·점핏은 처음 검색한 날엔 전부 새 공고로 보인다고 안내하세요. 여러 사이트에 함께 올라온 같은 공고는 한 줄로 합치고 나머지 링크는 also_on에 둡니다. ` +
1101
+ `참고: 외부로 보내는 것은 검색어·직무 조건뿐이고 사용자 데이터는 전송하지 않으며, 봇 차단 우회는 하지 않습니다. 로컬에는 처음 본 날짜(사이트·공고 번호·날짜)만 기록합니다. 잡코리아는 robots.txt 제약으로 포함되지 않습니다. 결과가 비어 있으면 키워드나 직무 조건을 바꾸거나 사용자에게 공고 URL/본문을 받아 save_job_posting으로 저장하세요.`,
1089
1102
  inputSchema: {
1090
- keyword: z.string().describe('검색어(직무·기술·키워드). 예: "백엔드", "React 신입"'),
1103
+ keyword: z.string().optional().describe('검색어(직무·기술·키워드). 예: "영업", "React 신입". 직무 조건만 쓰거나 저장된 조건을 쓸 땐 생략'),
1091
1104
  sources: z.array(z.enum([...JOB_SEARCH_SOURCES] as [string, ...string[]])).optional().describe(`대상 사이트(생략 시 전체: ${JOB_SEARCH_SOURCES.join('·')})`),
1092
- limit: z.number().optional().describe('사이트별 최대 건수(기본 10, 최대 50)'),
1105
+ limit: z.number().optional().describe('사이트별 최대 건수(기본 10, posted_since가 있으면 50, 최대 100). 오늘 공고가 limit만큼 꽉 차서 돌아오면 더 있을 수 있으니 늘려서 다시 검색'),
1106
+ remember_categories: z.array(z.string()).max(20).optional().describe('리멤버 직무(find_job_categories 결과): "상위>하위" 또는 상위만. 예: ["재무·회계·세무>회계결산"]'),
1107
+ saramin_categories: z.array(z.string().regex(/^\d+$/)).max(20).optional().describe('사람인 직무 코드(find_job_categories 결과). 예: ["322"]'),
1108
+ posted_since: z
1109
+ .string()
1110
+ .refine((s) => s === 'today' || isYmd(s), '"today" 또는 실제 날짜(YYYY-MM-DD)')
1111
+ .optional()
1112
+ .describe('"today" 또는 YYYY-MM-DD. 그날 이후 공고만'),
1093
1113
  },
1114
+ // 공고를 가져오기만 한다. 처음 본 날짜 기록은 사용자 데이터가 아닌 로컬 캐시라 readOnly로 둔다.
1094
1115
  readOnly: true,
1095
1116
  handler: async (args) => {
1096
- const res = await searchJobs(args.keyword, { sources: args.sources, limit: args.limit });
1117
+ let keyword = (args.keyword ?? '').trim();
1118
+ let rememberCategories = args.remember_categories ?? [];
1119
+ let saraminCategories = args.saramin_categories ?? [];
1120
+ let usedSaved = false;
1121
+ if (!keyword && !rememberCategories.length && !saraminCategories.length) {
1122
+ const saved = getJobSearchPreferences();
1123
+ if (!saved.keyword && !saved.remember_categories.length && !saved.saramin_categories.length) {
1124
+ return fail(
1125
+ '검색어나 직무 조건이 없고 저장된 검색 조건도 없습니다. 사용자의 희망 직무를 find_job_categories로 찾아 set_job_search_preferences로 저장하거나, keyword를 주세요.',
1126
+ );
1127
+ }
1128
+ ({ keyword, remember_categories: rememberCategories, saramin_categories: saraminCategories } = saved);
1129
+ usedSaved = true;
1130
+ }
1131
+ const today = new Date().toLocaleDateString('sv-SE');
1132
+ const res = await searchJobs(keyword, {
1133
+ sources: args.sources,
1134
+ limit: args.limit,
1135
+ rememberCategories,
1136
+ saraminCategories,
1137
+ postedSince: args.posted_since === 'today' ? today : args.posted_since,
1138
+ today,
1139
+ });
1097
1140
  const failed = res.providers.filter((p) => !p.ok);
1098
1141
  const okCount = res.results.length;
1099
- const parts = [`'${res.query}' 검색 결과 ${okCount}건`];
1100
- if (failed.length) parts.push(`(일부 사이트 응답 실패: ${failed.map((f) => f.source).join(', ')})`);
1101
- if (okCount === 0) parts.push('— 키워드를 바꾸거나, 공고 URL/본문을 주시면 직접 저장해 드릴게요.');
1102
- return ok(parts.join(' '), res);
1142
+ const parts = [`'${res.query || '직무 조건'}' 검색 결과 ${okCount}건${usedSaved ? '(저장된 검색 조건 사용)' : ''}`];
1143
+ if (res.filtered_out) parts.push(`(게시일 기준 ${res.filtered_out}건 제외)`);
1144
+ if (failed.length) parts.push(`(일부 사이트 응답 실패: ${failed.map((f) => `${f.source}: ${f.error ?? f.status}`).join('; ')})`);
1145
+ if (okCount === 0) parts.push('- 키워드나 직무 조건을 바꾸거나, 공고 URL/본문을 주시면 직접 저장해 드릴게요.');
1146
+ return ok(parts.join(' '), {
1147
+ ...res,
1148
+ used_saved_preferences: usedSaved,
1149
+ conditions: { keyword, remember_categories: rememberCategories, saramin_categories: saraminCategories },
1150
+ });
1151
+ },
1152
+ },
1153
+ {
1154
+ name: 'find_job_categories',
1155
+ title: '직무 분류 찾기(리멤버·사람인)',
1156
+ description:
1157
+ '사용자의 직무를 리멤버 직무 분류("상위>하위")와 사람인 직무 코드로 바꿔 줍니다. CareerMate에 내장된 분류표에서 찾으므로 외부 통신이 없습니다. search_jobs나 set_job_search_preferences에 직무 조건을 넣기 전에 호출하세요. 영업·회계·디자인·간호·생산관리 등 모든 직군을 다룹니다. 여러 직무는 쉼표로 구분합니다(예: "경리, 회계"). 결과는 관련도순이니, 그대로 다 넣지 말고 사용자 직무에 맞는 것만 골라 쓰세요. 같은 이름이 다른 분야에 있을 수 있으므로(예: 사람인 "프론트"는 호텔 프런트 데스크) group·kind를 보고 고르세요. 사용자가 직무를 말하지 않았으면 프로필의 희망 직무(desired_roles)를 쓰세요.',
1158
+ inputSchema: {
1159
+ query: z.string().describe('직무 이름(쉼표로 여러 개). 예: "데이터 분석가", "경리, 회계", "간호사"'),
1160
+ limit: z.number().int().min(1).max(100).optional().describe('사이트별 최대 후보 수(기본 20)'),
1161
+ },
1162
+ readOnly: true,
1163
+ handler: (args) => {
1164
+ const r = findJobCategories(args.query, args.limit ?? 20);
1165
+ if (!r.remember.length && !r.saramin.length) {
1166
+ return ok(`'${args.query}'에 맞는 직무 분류를 찾지 못했습니다. 더 일반적인 이름(예: "개발자" 대신 "개발", "웹개발")으로 다시 찾거나 keyword 검색을 쓰세요.`, r);
1167
+ }
1168
+ return ok(`직무 분류 후보: 리멤버 ${r.remember.length}개, 사람인 ${r.saramin.length}개. 사용자 직무에 맞는 것만 골라 쓰세요.`, r);
1169
+ },
1170
+ },
1171
+ {
1172
+ name: 'get_job_search_preferences',
1173
+ title: '저장된 공고 검색 조건 보기',
1174
+ description:
1175
+ 'set_job_search_preferences로 저장해 둔 공고 검색 조건(키워드·리멤버 직무·사람인 코드)을 돌려줍니다. 사용자가 "내 검색 조건 뭐야?"라고 묻거나, 저장된 조건으로 search_jobs를 돌리기 전에 확인할 때 쓰세요.',
1176
+ inputSchema: {},
1177
+ readOnly: true,
1178
+ handler: () => {
1179
+ const p = getJobSearchPreferences();
1180
+ const empty = !p.keyword && !p.remember_categories.length && !p.saramin_categories.length;
1181
+ return ok(empty ? '저장된 공고 검색 조건이 없습니다.' : '저장된 공고 검색 조건입니다.', p);
1182
+ },
1183
+ },
1184
+ {
1185
+ name: 'set_job_search_preferences',
1186
+ idempotent: true, // 같은 값 재저장은 무해
1187
+ title: '공고 검색 조건 저장',
1188
+ description:
1189
+ '사용자가 자주 볼 공고 검색 조건을 저장합니다. 저장해 두면 search_jobs를 keyword·직무 조건 없이 불러도 이 조건으로 검색합니다("오늘 새 공고 보여줘"를 매번 다시 묻지 않게). 직무 조건은 find_job_categories로 찾은 값만 넣고, 저장 전에 사용자에게 어떤 직무로 저장할지 확인하세요. 준 필드만 바뀌고, 빈 배열/빈 문자열을 주면 그 항목을 비웁니다.',
1190
+ inputSchema: {
1191
+ keyword: z.string().optional().describe('기본 검색어(선택). 예: "React"'),
1192
+ remember_categories: z.array(z.string()).max(20).optional().describe('리멤버 직무("상위>하위" 또는 상위만)'),
1193
+ saramin_categories: z.array(z.string().regex(/^\d+$/)).max(20).optional().describe('사람인 직무 코드'),
1194
+ },
1195
+ handler: (args) => {
1196
+ const bad = [
1197
+ ...((args.remember_categories ?? []) as string[]).filter((c) => !isRememberCategory(c)),
1198
+ ...((args.saramin_categories ?? []) as string[]).filter((c) => !isSaraminCategory(c)),
1199
+ ];
1200
+ if (bad.length) return fail(`리멤버·사람인 분류표에 없는 값입니다: ${bad.join(', ')}. find_job_categories로 찾은 값을 넣어 주세요.`);
1201
+ const p = setJobSearchPreferences({
1202
+ ...(args.keyword !== undefined ? { keyword: args.keyword.trim() } : {}),
1203
+ ...(args.remember_categories ? { remember_categories: args.remember_categories } : {}),
1204
+ ...(args.saramin_categories ? { saramin_categories: args.saramin_categories } : {}),
1205
+ });
1206
+ return ok('공고 검색 조건을 저장했습니다. 이제 "오늘 올라온 공고 보여줘"라고 하면 이 조건으로 찾아요.', p);
1103
1207
  },
1104
1208
  },
1105
1209
  {
@@ -1,67 +1,24 @@
1
1
  /**
2
- * @careermate/prompts — system.ts
2
+ * Global MCP operating rules.
3
3
  *
4
- * CAREERMATE_SYSTEM_PROMPT teaches an AI assistant (ChatGPT / Claude / Gemini)
5
- * HOW to behave as a good CareerMate operator. CareerMate itself contains no LLM;
6
- * the user's own AI does all reading, analysis, and writing through MCP tools.
7
- *
8
- * Written in Korean: it is read both by Korean users and by the AI on their behalf.
4
+ * Keep this prompt intentionally short. Detailed sequencing belongs in
5
+ * get_workflow_guide/context responses so clients do not receive duplicated or
6
+ * conflicting routing instructions on every turn.
9
7
  */
10
-
11
- export const CAREERMATE_SYSTEM_PROMPT = `당신은 사용자의 'CareerMate' 커리어 비서입니다.
12
-
13
- CareerMate는 사용자의 컴퓨터에서 동작하는 로컬 커리어 관리 도구이며, MCP(툴 호출)를 통해 사용자의 프로필·이력서·자기소개서·지원 공고·지원 현황 데이터를 읽고 씁니다. CareerMate 안에는 별도의 AI가 없습니다. 모든 분석·작성·요약은 '당신'(사용자의 AI)이 MCP 툴을 호출해 수행합니다.
14
-
15
- # 핵심 행동 원칙
16
-
17
- 1. 항상 맥락을 먼저 불러옵니다.
18
- - 공고를 분석하거나 자기소개서를 쓰기 전에는 반드시 \`get_application_context\`를 먼저 호출하세요. 이 한 번의 호출로 프로필, 대표 이력서, 경력(experiences), 프로젝트(projects), 스킬(skills), 기존 자기소개서, 최근 지원 현황, 대상 공고(job_id 전달 시), 이전 적합도 분석, 같은 회사/직무 관련 기록, 그리고 사용자의 글쓰기 선호(선호 문체 preferred_tone + 강조 포인트 emphasis_points)를 한꺼번에 받습니다.
19
- - 일반적인 상태 확인이 필요하면 \`get_onboarding_status\`, \`get_profile\`, \`get_resumes\`, \`get_cover_letters\`, \`list_recent_activity\` 등 읽기 툴을 적극 사용하세요.
20
- - 기억에 의존하지 말고, 매 작업 시작 시 실제 데이터를 다시 불러오세요.
21
-
22
- 2. 결과는 반드시 다시 저장합니다.
23
- - 분석·작성한 내용은 머릿속이나 대화창에만 남기지 말고 해당 저장 툴로 CareerMate에 기록하세요: 공고는 \`save_job_posting\`, 적합도 분석은 \`save_fit_analysis\`, 자기소개서 버전은 \`save_cover_letter_version\`, 경력기술서는 \`add_resume\`(kind=career_description), 면접 준비는 \`save_interview_prep\`, 상태 변경은 \`update_application_status\`.
24
- - 여러 문서를 한 번에 요청받아도 각 문서는 각자의 저장 툴로 따로 저장하세요. 경력기술서와 자기소개서를 한 턴에 함께 만들었더라도 한 저장 호출의 본문에 다른 문서를 이어붙이지 마세요(경력기술서=\`add_resume\` kind=career_description, 자기소개서=\`save_cover_letter_version\`로 각각 분리 저장).
25
- - 저장 후에는 무엇이 저장되었는지 사용자에게 간단히 알려주세요.
26
-
27
- 3. 데이터를 지어내지 않습니다.
28
- - 프로필·경력·성과·수치·회사명 등은 사용자가 제공했거나 CareerMate에 저장된 것만 사용하세요. 불확실하면 추측하지 말고 사용자에게 물어보세요.
29
- - 자기소개서에 검증되지 않은 경험이나 과장된 성과를 넣지 마세요. 빈칸이 있으면 "이 부분은 확인이 필요합니다"라고 표시하고 사용자에게 확인을 요청하세요.
30
-
31
- 4. 로컬-퍼스트 / 프라이버시를 존중합니다.
32
- - 모든 데이터는 사용자의 컴퓨터에만 저장됩니다. 데이터를 외부로 보내거나 다른 용도로 쓰지 마세요.
33
- - 민감 정보(연락처, 주소 등)는 꼭 필요한 작업에만 사용하고 불필요하게 노출하지 마세요.
34
-
35
- 5. 자기소개서는 작성 전 확인을 받습니다.
36
- - 자기소개서나 경력기술서를 새로 쓰거나 크게 고치기 전에, 어떤 공고/직무를 대상으로 어떤 톤과 강조점으로 쓸지 사용자에게 먼저 확인하세요.
37
- - 작성 후에는 \`save_cover_letter_version\`으로 버전을 저장하고, 변경 요약을 \`note\`에 남기세요. 사용자가 원하면 \`export_cover_letter\`로 내보내세요.
38
-
39
- 6. 사용자에게는 사용자의 언어로 쉽게 알립니다.
40
- - **응답 언어 = 사용자의 언어.** 사용자가 한국어로 말하면 한국어로, 영어 등 다른 언어로 말하면 그 언어로 답합니다. 이 지침·도구 응답·플레이북이 한국어로 적혀 있어도 그대로 복사하지 말고 사용자 언어로 옮겨 전달하세요(사실·수치·고유명사는 보존).
41
- - "MCP", "툴 호출", "job_id" 같은 기술 용어는 사용자에게 직접 노출하지 마세요. "공고를 저장했어요", "적합도를 분석했어요"처럼 일상적인 표현으로 진행 상황을 알리세요.
42
- - 도구 응답에 \`user_message\`가 있으면 사용자에게 보고할 때 그 내용을 우선 활용하되, 사용자가 한국어가 아닌 언어로 대화하면 한국어 원문을 그대로 복사하지 말고 사용자 언어로 옮겨 전달하세요. \`id\`, \`job_id\`, \`cover_letter_id\`, \`workflow_id\`, JSON 원문, 도구 이름은 내부 작업용으로만 쓰고 사용자에게 그대로 복사하지 마세요.
43
- - 도구 응답에 \`suggested_next_action\`이 있으면 다음 행동을 강요하지 말고 선택지로 제안하세요.
44
- - 무엇을 했고, 무엇을 알게 되었고, 다음에 무엇을 할 수 있는지 짧고 명확하게 전달하세요.
45
-
46
- 7. 다음 단계를 먼저 제안합니다.
47
- - 작업이 끝나면 자연스러운 다음 행동을 제안하세요. 예: 공고 분석 후 → "이 공고에 맞춘 자기소개서를 써드릴까요?", 경력기술서 저장 후 → "이 공고에 맞춰 자기소개서도 정리할까요?", 자기소개서 저장 후 → "지원 상태를 '지원 완료'로 바꿀까요?", 상태가 '서류 합격'이 되면 → "면접 준비를 도와드릴까요?".
48
- - 공고 분석 후에는 곧장 자기소개서를 쓰지 말고, 반드시 선택지를 보여주세요: "1. 네, 이 공고에 맞춘 자기소개서를 써줘" / "2. 아니요, 분석만 저장해줘". 사용자가 1번을 고른 경우에만 자기소개서 작성 흐름으로 이어갑니다.
49
- - \`get_onboarding_status\`의 next_steps나 \`open_dashboard\`/\`open_application\`을 활용해 사용자를 다음 행동으로 안내하세요.
50
-
51
- 8. 업데이트 요청은 명확하게 처리합니다.
52
- - 사용자가 "최신이야?", "업데이트 있어?"처럼 확인만 요청하면 \`check_for_update\`를 호출하고 결과를 쉬운 말로 알려주세요.
53
- - 사용자가 "업데이트해줘", "CareerMate 업데이트해줘", "최신으로 올려줘"처럼 적용을 명시하면 \`update_careermate\`를 바로 호출하세요. 이 도구가 최신 버전 확인도 함께 합니다.
54
- - 업데이트 성공 후에는 적용하려면 AI 앱 또는 MCP 연결을 재시작해야 한다고 안내하세요. 사용자 커리어 데이터는 로컬에 보존되고, npm에는 공개 버전 정보 확인/설치 요청만 나갑니다.
55
-
56
- 9. 글은 AI 티 안 나게 씁니다.
57
- - 자기소개서·자기 PR·지원 메일 등 사람이 쓴 듯한 글을 작성하기 직전에는 \`get_writing_style_guide\`를 호출해 글쓰기 규칙을 가져오고 그대로 적용하세요.
58
- - 번역투, "열정을 가지고/귀사의 발전에 기여" 같은 클리셰, 기계적 병렬(첫째·둘째·셋째), 상투적 연결어, 균일한 문장 리듬을 피하고 담백하고 구체적으로 씁니다. 단, 사실·수치·고유명사는 절대 바꾸거나 지어내지 않습니다. 이 AI-tell 목록은 한국어 글 기준이며, 다른 언어로 쓸 때는 같은 원리를 그 언어의 자연스러운 관습으로 적용합니다(산출물 언어·관행은 대상 공고·시장을 따름).
59
-
60
- 10. 전문가 절차로 만들고, 저장 전 당신이 자가검증합니다.
61
- - 공고 분석·자소서·경력기술서·면접 자료처럼 사용자 대면 결과를 만들기 전에는, 즉답하지 말고 \`get_workflow_guide\`(workflow_id 지정)를 먼저 호출해 그 작업의 전문가 실행 절차(EOP)와 적용할 플레이북·검증기 순서를 받으세요. 이어 \`get_playbook\`으로 도메인 지식을 받아 작성하고, save로 저장하기 직전 \`get_verifier\`로 검증 루브릭을 받아 **당신이 직접** 점검·수정한 뒤 저장합니다.
62
- - CareerMate는 슬롭 밀도·키워드 매칭처럼 셀 수 있는 항목만 보조로 돕고, 의미 판단(이 글이 질문에 답하는가·톤이 맞는가)은 당신이 합니다. 사실·수치·고유명사는 그대로 보존합니다.
63
-
64
- # 작업 방식 요약
65
- - 읽기(get_*) → 분석/작성 → 저장(save_*/update_*) → 사용자에게 쉬운 말로 보고 → 다음 단계 제안.
66
- - 8가지 지원 상태: 작성 중(draft), 지원 예정(planned), 지원 완료(applied), 서류 합격(document_passed), 면접 진행(interview), 최종 합격(final_passed), 불합격(rejected), 보류(on_hold).
67
- - 의심스러우면 멈추고 묻습니다. 사용자의 커리어가 걸린 일이므로 정확함이 속도보다 중요합니다.`;
8
+ export const CAREERMATE_SYSTEM_PROMPT = `당신은 사용자의 CareerMate 커리어 비서입니다. CareerMate는 사용자의 로컬 데이터베이스와 문서 저장소이며, 분석과 작성은 연결된 AI가 수행합니다.
9
+
10
+ # 불변 규칙
11
+ 1. 작업에 필요한 사용자·공고 컨텍스트만 먼저 조회합니다. 전체 데이터를 습관적으로 불러오지 않습니다.
12
+ 2. 회사, 역할, 기술, 기간, 수치, 성과를 만들거나 과장하지 않습니다. 공고 요구사항을 사용자 경험처럼 쓰지 않습니다.
13
+ 3. 사용자 확인 사실, AI가 구조화한 미확인 정보, AI의 추정을 명확히 구분합니다.
14
+ 4. 저장 전 사실·수치·기간·개인 기여·문서 간 일관성을 검증합니다. 불확실한 내용은 문장에 넣지 말고 최대 3~5개의 구체적인 확인 질문으로 반환합니다.
15
+ 5. 결과물에는 사용한 근거, 미확인 항목, 위험 또는 경고를 함께 반환합니다. 수치는 확인된 구조화 데이터나 source excerpt가 있을 때만 사용합니다.
16
+ 6. 서로 다른 문서는 각 전용 저장 도구를 사용합니다. 사용자가 저장을 요청하지 않았거나 초안 검토만 요청한 경우 임의로 저장하지 않습니다.
17
+ 7. 삭제·초기화·복원·덮어쓰기·업데이트처럼 데이터나 실행 환경을 바꾸는 작업은 영향과 대상을 설명하고 사용자의 명시적 확인 후 실행합니다.
18
+ 8. 외부 통신이 필요한 기능은 로컬 작업과 구분해 알립니다. 저장된 커리어 원문이나 개인정보를 외부로 보내지 않습니다.
19
+ 9. 사용자 언어와 대상 문서의 언어를 유지합니다. 내부 ID, 도구 이름, 원시 JSON, 로컬 경로, SQL 및 스택 추적을 사용자에게 노출하지 않습니다.
20
+ 10. 구체 절차와 다음 도구는 get_workflow_guide 또는 현재 context 응답의 recommended route를 따릅니다. 같은 조회·검증 도구를 이유 없이 반복 호출하지 않습니다.
21
+ 11. 이력서·자기소개서·면접 답변은 사용자가 실제 면접에서 설명 가능한 수준으로 작성하며, 팀 성과와 개인 기여를 구분합니다.
22
+ 12. 글은 화려함보다 구체성·사실성·가독성을 우선합니다. 번역투, 클리셰, 키워드 도배, 균일한 AI 문체를 피하되 사실은 바꾸지 않습니다.
23
+
24
+ 사용자에게는 무엇을 확인했고 무엇이 미확인인지 짧고 명확하게 설명합니다.`;
package/scripts/init.ts CHANGED
@@ -399,6 +399,8 @@ const SAFE_TOOLS: readonly string[] = [
399
399
  // search_jobs: 공개 잡보드에서 공고를 가져오기만(보내는 건 검색어뿐, 사용자 데이터 미전송).
400
400
  // check_for_update와 같은 "네트워크 read" 부류라 사전허용 안전(우회·쓰기 없음).
401
401
  'search_jobs',
402
+ // 직무 분류는 내장 분류표 조회, 검색 조건은 로컬 저장/조회뿐(외부 전송 없음) → 사전허용 안전.
403
+ 'find_job_categories', 'get_job_search_preferences', 'set_job_search_preferences',
402
404
  // 오퍼(제안) — 로컬 저장/조회(save_fit_analysis 등과 동급), 외부 전송 없음.
403
405
  'save_offer', 'compare_offers', 'get_offer',
404
406
  // careermate 자신의 인박스 폴더·대시보드(127.0.0.1)만 여는 로컬 동작 + 인박스 내부 문서 읽기 → 사전허용 안전.
package/tsconfig.json CHANGED
@@ -13,17 +13,16 @@
13
13
  "forceConsistentCasingInFileNames": true,
14
14
  "allowImportingTsExtensions": true,
15
15
  "noEmit": true,
16
- "baseUrl": ".",
17
16
  "paths": {
18
- "@careermate/shared": ["packages/shared/src/index.ts"],
19
- "@careermate/db": ["packages/db/src/index.ts"],
20
- "@careermate/core": ["packages/core/src/index.ts"],
21
- "@careermate/mcp-tools": ["packages/mcp-tools/src/index.ts"],
22
- "@careermate/exporters": ["packages/exporters/src/index.ts"],
23
- "@careermate/parsers": ["packages/parsers/src/index.ts"],
24
- "@careermate/prompts": ["packages/prompts/src/index.ts"],
25
- "@careermate/workflows": ["packages/workflows/src/index.ts"],
26
- "@careermate/knowledge": ["packages/knowledge/src/index.ts"]
17
+ "@careermate/shared": ["./packages/shared/src/index.ts"],
18
+ "@careermate/db": ["./packages/db/src/index.ts"],
19
+ "@careermate/core": ["./packages/core/src/index.ts"],
20
+ "@careermate/mcp-tools": ["./packages/mcp-tools/src/index.ts"],
21
+ "@careermate/exporters": ["./packages/exporters/src/index.ts"],
22
+ "@careermate/parsers": ["./packages/parsers/src/index.ts"],
23
+ "@careermate/prompts": ["./packages/prompts/src/index.ts"],
24
+ "@careermate/workflows": ["./packages/workflows/src/index.ts"],
25
+ "@careermate/knowledge": ["./packages/knowledge/src/index.ts"]
27
26
  }
28
27
  },
29
28
  "include": ["packages/**/*.ts", "apps/**/*.ts", "scripts/**/*.ts"],