careermate 0.0.10 → 0.0.13
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/AGENTS.md +3 -2
- package/INSTALL.md +6 -6
- package/LICENSE +1 -1
- package/README.md +18 -16
- package/dist/career-os/README.md +2 -2
- package/dist/doctor.mjs +597 -59
- package/dist/mcp.mjs +624 -60
- package/dist/migrate.mjs +23 -6
- package/dist/public/app.js +1 -7
- package/dist/public/i18n/en/settings.js +1 -6
- package/dist/public/i18n/ko/settings.js +1 -6
- package/dist/public/index.html +0 -1
- package/dist/public/lib.js +13 -4
- package/dist/public/pages/documents.js +18 -5
- package/dist/public/pages/home.js +3 -1
- package/dist/public/pages/settings.js +13 -12
- package/dist/public/styles.css +30 -0
- package/dist/web.mjs +332 -17
- package/docs/FAQ.md +3 -4
- package/docs/LOCAL_FIRST.md +1 -1
- package/docs/MCP_TOOLS.md +6 -3
- package/docs/career-os/README.md +2 -2
- package/package.json +1 -8
- package/packages/core/src/index.ts +1 -0
- package/packages/core/src/job-categories.data.ts +140 -0
- package/packages/core/src/job-categories.ts +106 -0
- package/packages/core/src/jobsearch.ts +292 -21
- package/packages/db/src/index.ts +1 -0
- package/packages/db/src/interview-retrospectives.ts +2 -6
- package/packages/db/src/job-search.ts +100 -0
- package/packages/db/src/repositories.ts +1 -0
- package/packages/mcp-tools/src/tools.ts +115 -11
- package/scripts/init.ts +2 -0
|
@@ -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
|
-
// 외부 공개 잡보드(
|
|
1094
|
+
// 외부 공개 잡보드(원티드·점핏·리멤버·사람인)로 검색어·직무 조건이 나가므로 openWorld를 정직하게 표시.
|
|
1084
1095
|
openWorld: true,
|
|
1085
|
-
title: '공고 검색·가져오기(
|
|
1096
|
+
title: '공고 검색·가져오기(키워드·직무·신규)',
|
|
1086
1097
|
description:
|
|
1087
|
-
|
|
1088
|
-
|
|
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('검색어(직무·기술·키워드). 예: "
|
|
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, 최대
|
|
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
|
-
|
|
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 (
|
|
1101
|
-
if (
|
|
1102
|
-
|
|
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
|
{
|
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)만 여는 로컬 동작 + 인박스 내부 문서 읽기 → 사전허용 안전.
|