careermate 0.0.13 → 0.0.14
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 +1 -1
- package/dist/doctor.mjs +24 -6
- package/dist/mcp.mjs +24 -6
- package/dist/web.mjs +1 -1
- package/docs/MCP_TOOLS.md +1 -1
- package/package.json +1 -1
- package/packages/core/src/jobsearch.ts +26 -3
- package/packages/mcp-tools/src/tools.ts +10 -2
package/AGENTS.md
CHANGED
|
@@ -16,7 +16,7 @@ CareerMate는 사용자 PC에만 데이터를 두는 **로컬 커리어 작업
|
|
|
16
16
|
- **공고 분석·자소서 작성 전에는 반드시 `get_application_context`를 먼저 호출하라.** 한 번의 호출로 프로필·대표 이력서·경력·스킬·기존 자소서·대상 공고·선호 문체·강조 포인트를 모두 돌려준다. 맥락 없이 작성하지 마라.
|
|
17
17
|
- **결과는 다시 CareerMate에 저장하라.** 분석 → `save_fit_analysis`, 자소서 → `save_cover_letter_version`, 경력기술서 → `add_resume`(`kind=career_description`), 공고 → `save_job_posting`, 면접 자료 → `save_interview_prep`. 커리어 데이터는 흩어진 파일이 아니라 CareerMate 안에 둔다. 임의의 로컬 파일 생성은 최소화하라(사용자가 파일을 원하면 자소서는 `export_cover_letter`, 이력서·경력기술서는 `export_resume`, 프로필은 `export_profile`로 `~/.careermate/exports`에 내보낸다).
|
|
18
18
|
- **여러 문서를 한 번에 요청받아도 각 문서는 각자의 저장 도구로 따로 저장하라.** "경력기술서랑 자기소개서 둘 다 써줘"처럼 한 턴에 여러 문서를 만들었더라도, 한 저장 호출의 본문(content)에 다른 문서를 이어붙이지 마라 — 경력기술서는 `add_resume`(`kind=career_description`), 자기소개서는 `save_cover_letter_version`으로 **각각 따로** 저장한다(작성은 한 답변에 둘 다 보여줘도 무방하나, 저장은 문서별 도구로 분리). 한 문서 본문에 다른 문서가 섞이면 대시보드에 통째로 붙어 등록된다.
|
|
19
|
-
- **공고 찾기는 사용자 직무 기준으로.** 직무 조건은 짐작하지 말고 `find_job_categories`로 사용자 직무(말하지 않았으면 프로필의 희망 직무)에 맞는 리멤버 분류·사람인 코드를 찾는다. 사용자가 자주 볼 직무라고 확인하면 `set_job_search_preferences`로 저장해 두고, 이후 "오늘 올라온 공고 보여줘"는 `search_jobs`를 조건 없이 `posted_since: "today"`로 부른다. 원티드·점핏은 게시일을 주지 않아 CareerMate가 처음 본 날로 거르므로 첫 검색에선 전부 새 공고로 보인다는 점을 알려라. 개발 직군만이 아니라 모든 직군에 같은 흐름을 쓴다.
|
|
19
|
+
- **공고 찾기는 사용자 직무 기준으로.** 직무 조건은 짐작하지 말고 `find_job_categories`로 사용자 직무(말하지 않았으면 프로필의 희망 직무)에 맞는 리멤버 분류·사람인 코드를 찾는다. 사용자가 자주 볼 직무라고 확인하면 `set_job_search_preferences`로 저장해 두고, 이후 "오늘 올라온 공고 보여줘"는 `search_jobs`를 조건 없이 `posted_since: "today"`로 부른다. 원티드·점핏은 게시일을 주지 않아 CareerMate가 처음 본 날로 거르므로 첫 검색에선 전부 새 공고로 보인다는 점을 알려라. 사용자가 "5년차"처럼 경력을 말하면 `years`(신입은 0)로 거른다. 개발 직군만이 아니라 모든 직군에 같은 흐름을 쓴다.
|
|
20
20
|
- **도구 응답은 사람 말로 바꿔라.** 응답에 `user_message`가 있으면 그 내용을 우선 활용하되 **사용자의 언어로 옮겨** 전달하고(한국어 원문을 그대로 복사하지 않는다), `suggested_next_action`은 선택지로 제안한다. `id`, `job_id`, `cover_letter_id`, `workflow_id`, JSON 원문, 도구 이름은 내부 작업용으로만 쓰고 사용자에게 그대로 복사하지 않는다.
|
|
21
21
|
- **공고 분석 뒤에는 자기소개서를 곧장 쓰지 말고 선택지를 보여줘라.** 적합도 분석을 저장하고 쉬운 말로 설명한 뒤 "이 공고에 맞춘 자기소개서를 써드릴까요?"라고 묻고, `1. 네, 이 공고에 맞춘 자기소개서를 써줘` / `2. 아니요, 분석만 저장해줘`처럼 고르게 한다. 사용자가 1번을 고른 경우에만 자소서 작성·저장 흐름으로 이어간다. **1번(쓰기)을 고르면 작성 전에, 회사가 지정한 문항·양식·글자수 제한이 있으면 먼저 달라고 요청하라** — 한국 자소서는 문항형(회사별 항목·글자수)이 많아 문항·양식이 결과 질을 좌우한다. 공고·맥락에 없으면 사용자에게 문항 텍스트·제출 양식·글자수(상·하한, 공백포함 여부)를 묻고, 끝내 없으면 일반 양식으로 진행한다고 알린다. 받은 문항·양식·글자수는 작성에 반영하고 가능하면 공고에 함께 저장한다.
|
|
22
22
|
- **표준 워크플로우를 따르라(핵심 6 + 선택 1)**: onboarding, analyze_job, write_cover_letter, write_career_description, manage_application_status, prepare_interview — 그리고 사용자가 LinkedIn·포트폴리오 같은 자기표현 자산을 요청하면 `build_personal_brand`. 순서가 헷갈리면 `get_workflow_guide`를 호출한다.
|
package/dist/doctor.mjs
CHANGED
|
@@ -86991,7 +86991,7 @@ var ApplicationContextSchema = external_exports.object({
|
|
|
86991
86991
|
});
|
|
86992
86992
|
|
|
86993
86993
|
// packages/shared/src/build-info.ts
|
|
86994
|
-
var APP_VERSION = true ? "0.0.
|
|
86994
|
+
var APP_VERSION = true ? "0.0.14" : "0.0.0-dev";
|
|
86995
86995
|
var BUNDLED = true;
|
|
86996
86996
|
|
|
86997
86997
|
// packages/shared/src/index.ts
|
|
@@ -89413,8 +89413,13 @@ async function searchJobs(query, opts = {}) {
|
|
|
89413
89413
|
}
|
|
89414
89414
|
results = dedupeAcrossBoards(results);
|
|
89415
89415
|
let filtered_out;
|
|
89416
|
-
|
|
89417
|
-
|
|
89416
|
+
const years = opts.years;
|
|
89417
|
+
if (opts.postedSince || years != null) {
|
|
89418
|
+
const kept = results.filter((r) => {
|
|
89419
|
+
if (opts.postedSince && (r.posted_at ?? r.first_seen_at ?? today) < opts.postedSince) return false;
|
|
89420
|
+
const range2 = years != null ? experienceRange(r.experience) : null;
|
|
89421
|
+
return !range2 || range2[0] <= years && years <= range2[1];
|
|
89422
|
+
});
|
|
89418
89423
|
filtered_out = results.length - kept.length;
|
|
89419
89424
|
results = kept;
|
|
89420
89425
|
}
|
|
@@ -89425,6 +89430,17 @@ async function searchJobs(query, opts = {}) {
|
|
|
89425
89430
|
...filtered_out !== void 0 ? { filtered_out } : {}
|
|
89426
89431
|
};
|
|
89427
89432
|
}
|
|
89433
|
+
function experienceRange(text) {
|
|
89434
|
+
const t = (text ?? "").replace(/\s/g, "");
|
|
89435
|
+
if (!t) return null;
|
|
89436
|
+
if (/무관/.test(t)) return [0, Infinity];
|
|
89437
|
+
const m = t.match(/(\d+)?~(\d+)년/);
|
|
89438
|
+
if (m) return [m[1] ? +m[1] : 0, +m[2]];
|
|
89439
|
+
const min2 = t.match(/(\d+)년(이상|↑)/);
|
|
89440
|
+
if (min2) return [/신입/.test(t) ? 0 : +min2[1], Infinity];
|
|
89441
|
+
if (/신입/.test(t)) return /경력/.test(t) ? [0, Infinity] : [0, 0];
|
|
89442
|
+
return null;
|
|
89443
|
+
}
|
|
89428
89444
|
var normCompany = (s) => s.replace(/\([^)]*\)|㈜|주식회사/g, "").toLowerCase().replace(/[^\p{L}\p{N}]/gu, "");
|
|
89429
89445
|
var normTitle = (title, company) => {
|
|
89430
89446
|
const c = normCompany(company);
|
|
@@ -110103,7 +110119,8 @@ ${retro_prompt}` : "");
|
|
|
110103
110119
|
limit: external_exports.number().optional().describe("\uC0AC\uC774\uD2B8\uBCC4 \uCD5C\uB300 \uAC74\uC218(\uAE30\uBCF8 10, posted_since\uAC00 \uC788\uC73C\uBA74 50, \uCD5C\uB300 100). \uC624\uB298 \uACF5\uACE0\uAC00 limit\uB9CC\uD07C \uAF49 \uCC28\uC11C \uB3CC\uC544\uC624\uBA74 \uB354 \uC788\uC744 \uC218 \uC788\uC73C\uB2C8 \uB298\uB824\uC11C \uB2E4\uC2DC \uAC80\uC0C9"),
|
|
110104
110120
|
remember_categories: external_exports.array(external_exports.string()).max(20).optional().describe('\uB9AC\uBA64\uBC84 \uC9C1\uBB34(find_job_categories \uACB0\uACFC): "\uC0C1\uC704>\uD558\uC704" \uB610\uB294 \uC0C1\uC704\uB9CC. \uC608: ["\uC7AC\uBB34\xB7\uD68C\uACC4\xB7\uC138\uBB34>\uD68C\uACC4\uACB0\uC0B0"]'),
|
|
110105
110121
|
saramin_categories: external_exports.array(external_exports.string().regex(/^\d+$/)).max(20).optional().describe('\uC0AC\uB78C\uC778 \uC9C1\uBB34 \uCF54\uB4DC(find_job_categories \uACB0\uACFC). \uC608: ["322"]'),
|
|
110106
|
-
posted_since: external_exports.string().refine((s) => s === "today" || isYmd(s), '"today" \uB610\uB294 \uC2E4\uC81C \uB0A0\uC9DC(YYYY-MM-DD)').optional().describe('"today" \uB610\uB294 YYYY-MM-DD. \uADF8\uB0A0 \uC774\uD6C4 \uACF5\uACE0\uB9CC')
|
|
110122
|
+
posted_since: external_exports.string().refine((s) => s === "today" || isYmd(s), '"today" \uB610\uB294 \uC2E4\uC81C \uB0A0\uC9DC(YYYY-MM-DD)').optional().describe('"today" \uB610\uB294 YYYY-MM-DD. \uADF8\uB0A0 \uC774\uD6C4 \uACF5\uACE0\uB9CC'),
|
|
110123
|
+
years: external_exports.number().int().min(0).max(40).optional().describe('\uC0AC\uC6A9\uC790 \uACBD\uB825 \uC5F0\uCC28(\uC2E0\uC785 0). \uACF5\uACE0\uC758 \uACBD\uB825 \uBC94\uC704\uC5D0 \uC774 \uC5F0\uCC28\uAC00 \uB4E4\uC5B4\uAC00\uB294 \uAC83\uB9CC \uB0A8\uAE40. "5\uB144\uCC28"\uBA74 5. \uC0AC\uC6A9\uC790\uAC00 \uB9D0\uD558\uC9C0 \uC54A\uC558\uC73C\uBA74 \uD504\uB85C\uD544 \uACBD\uB825\uC73C\uB85C \uACC4\uC0B0\uD574 \uB123\uB418 \uD655\uC778. \uACBD\uB825 \uD45C\uAE30\uB97C \uC77D\uC744 \uC218 \uC5C6\uB294 \uACF5\uACE0\uB294 \uB0A8\uAE40')
|
|
110107
110124
|
},
|
|
110108
110125
|
// 공고를 가져오기만 한다. 처음 본 날짜 기록은 사용자 데이터가 아닌 로컬 캐시라 readOnly로 둔다.
|
|
110109
110126
|
readOnly: true,
|
|
@@ -110129,18 +110146,19 @@ ${retro_prompt}` : "");
|
|
|
110129
110146
|
rememberCategories,
|
|
110130
110147
|
saraminCategories,
|
|
110131
110148
|
postedSince: args.posted_since === "today" ? today : args.posted_since,
|
|
110149
|
+
years: args.years,
|
|
110132
110150
|
today
|
|
110133
110151
|
});
|
|
110134
110152
|
const failed = res.providers.filter((p) => !p.ok);
|
|
110135
110153
|
const okCount = res.results.length;
|
|
110136
110154
|
const parts = [`'${res.query || "\uC9C1\uBB34 \uC870\uAC74"}' \uAC80\uC0C9 \uACB0\uACFC ${okCount}\uAC74${usedSaved ? "(\uC800\uC7A5\uB41C \uAC80\uC0C9 \uC870\uAC74 \uC0AC\uC6A9)" : ""}`];
|
|
110137
|
-
if (res.filtered_out) parts.push(`(\uAC8C\uC2DC\uC77C \
|
|
110155
|
+
if (res.filtered_out) parts.push(`(\uAC8C\uC2DC\uC77C\xB7\uACBD\uB825 \uC870\uAC74\uC73C\uB85C ${res.filtered_out}\uAC74 \uC81C\uC678)`);
|
|
110138
110156
|
if (failed.length) parts.push(`(\uC77C\uBD80 \uC0AC\uC774\uD2B8 \uC751\uB2F5 \uC2E4\uD328: ${failed.map((f) => `${f.source}: ${f.error ?? f.status}`).join("; ")})`);
|
|
110139
110157
|
if (okCount === 0) parts.push("- \uD0A4\uC6CC\uB4DC\uB098 \uC9C1\uBB34 \uC870\uAC74\uC744 \uBC14\uAFB8\uAC70\uB098, \uACF5\uACE0 URL/\uBCF8\uBB38\uC744 \uC8FC\uC2DC\uBA74 \uC9C1\uC811 \uC800\uC7A5\uD574 \uB4DC\uB9B4\uAC8C\uC694.");
|
|
110140
110158
|
return ok(parts.join(" "), {
|
|
110141
110159
|
...res,
|
|
110142
110160
|
used_saved_preferences: usedSaved,
|
|
110143
|
-
conditions: { keyword, remember_categories: rememberCategories, saramin_categories: saraminCategories }
|
|
110161
|
+
conditions: { keyword, remember_categories: rememberCategories, saramin_categories: saraminCategories, years: args.years }
|
|
110144
110162
|
});
|
|
110145
110163
|
}
|
|
110146
110164
|
},
|
package/dist/mcp.mjs
CHANGED
|
@@ -103447,7 +103447,7 @@ var ApplicationContextSchema = external_exports.object({
|
|
|
103447
103447
|
});
|
|
103448
103448
|
|
|
103449
103449
|
// packages/shared/src/build-info.ts
|
|
103450
|
-
var APP_VERSION = true ? "0.0.
|
|
103450
|
+
var APP_VERSION = true ? "0.0.14" : "0.0.0-dev";
|
|
103451
103451
|
var BUNDLED = true;
|
|
103452
103452
|
|
|
103453
103453
|
// packages/shared/src/index.ts
|
|
@@ -105869,8 +105869,13 @@ async function searchJobs(query, opts = {}) {
|
|
|
105869
105869
|
}
|
|
105870
105870
|
results = dedupeAcrossBoards(results);
|
|
105871
105871
|
let filtered_out;
|
|
105872
|
-
|
|
105873
|
-
|
|
105872
|
+
const years = opts.years;
|
|
105873
|
+
if (opts.postedSince || years != null) {
|
|
105874
|
+
const kept = results.filter((r) => {
|
|
105875
|
+
if (opts.postedSince && (r.posted_at ?? r.first_seen_at ?? today) < opts.postedSince) return false;
|
|
105876
|
+
const range2 = years != null ? experienceRange(r.experience) : null;
|
|
105877
|
+
return !range2 || range2[0] <= years && years <= range2[1];
|
|
105878
|
+
});
|
|
105874
105879
|
filtered_out = results.length - kept.length;
|
|
105875
105880
|
results = kept;
|
|
105876
105881
|
}
|
|
@@ -105881,6 +105886,17 @@ async function searchJobs(query, opts = {}) {
|
|
|
105881
105886
|
...filtered_out !== void 0 ? { filtered_out } : {}
|
|
105882
105887
|
};
|
|
105883
105888
|
}
|
|
105889
|
+
function experienceRange(text) {
|
|
105890
|
+
const t = (text ?? "").replace(/\s/g, "");
|
|
105891
|
+
if (!t) return null;
|
|
105892
|
+
if (/무관/.test(t)) return [0, Infinity];
|
|
105893
|
+
const m = t.match(/(\d+)?~(\d+)년/);
|
|
105894
|
+
if (m) return [m[1] ? +m[1] : 0, +m[2]];
|
|
105895
|
+
const min2 = t.match(/(\d+)년(이상|↑)/);
|
|
105896
|
+
if (min2) return [/신입/.test(t) ? 0 : +min2[1], Infinity];
|
|
105897
|
+
if (/신입/.test(t)) return /경력/.test(t) ? [0, Infinity] : [0, 0];
|
|
105898
|
+
return null;
|
|
105899
|
+
}
|
|
105884
105900
|
var normCompany = (s) => s.replace(/\([^)]*\)|㈜|주식회사/g, "").toLowerCase().replace(/[^\p{L}\p{N}]/gu, "");
|
|
105885
105901
|
var normTitle = (title, company) => {
|
|
105886
105902
|
const c = normCompany(company);
|
|
@@ -126571,7 +126587,8 @@ ${retro_prompt}` : "");
|
|
|
126571
126587
|
limit: external_exports.number().optional().describe("\uC0AC\uC774\uD2B8\uBCC4 \uCD5C\uB300 \uAC74\uC218(\uAE30\uBCF8 10, posted_since\uAC00 \uC788\uC73C\uBA74 50, \uCD5C\uB300 100). \uC624\uB298 \uACF5\uACE0\uAC00 limit\uB9CC\uD07C \uAF49 \uCC28\uC11C \uB3CC\uC544\uC624\uBA74 \uB354 \uC788\uC744 \uC218 \uC788\uC73C\uB2C8 \uB298\uB824\uC11C \uB2E4\uC2DC \uAC80\uC0C9"),
|
|
126572
126588
|
remember_categories: external_exports.array(external_exports.string()).max(20).optional().describe('\uB9AC\uBA64\uBC84 \uC9C1\uBB34(find_job_categories \uACB0\uACFC): "\uC0C1\uC704>\uD558\uC704" \uB610\uB294 \uC0C1\uC704\uB9CC. \uC608: ["\uC7AC\uBB34\xB7\uD68C\uACC4\xB7\uC138\uBB34>\uD68C\uACC4\uACB0\uC0B0"]'),
|
|
126573
126589
|
saramin_categories: external_exports.array(external_exports.string().regex(/^\d+$/)).max(20).optional().describe('\uC0AC\uB78C\uC778 \uC9C1\uBB34 \uCF54\uB4DC(find_job_categories \uACB0\uACFC). \uC608: ["322"]'),
|
|
126574
|
-
posted_since: external_exports.string().refine((s) => s === "today" || isYmd(s), '"today" \uB610\uB294 \uC2E4\uC81C \uB0A0\uC9DC(YYYY-MM-DD)').optional().describe('"today" \uB610\uB294 YYYY-MM-DD. \uADF8\uB0A0 \uC774\uD6C4 \uACF5\uACE0\uB9CC')
|
|
126590
|
+
posted_since: external_exports.string().refine((s) => s === "today" || isYmd(s), '"today" \uB610\uB294 \uC2E4\uC81C \uB0A0\uC9DC(YYYY-MM-DD)').optional().describe('"today" \uB610\uB294 YYYY-MM-DD. \uADF8\uB0A0 \uC774\uD6C4 \uACF5\uACE0\uB9CC'),
|
|
126591
|
+
years: external_exports.number().int().min(0).max(40).optional().describe('\uC0AC\uC6A9\uC790 \uACBD\uB825 \uC5F0\uCC28(\uC2E0\uC785 0). \uACF5\uACE0\uC758 \uACBD\uB825 \uBC94\uC704\uC5D0 \uC774 \uC5F0\uCC28\uAC00 \uB4E4\uC5B4\uAC00\uB294 \uAC83\uB9CC \uB0A8\uAE40. "5\uB144\uCC28"\uBA74 5. \uC0AC\uC6A9\uC790\uAC00 \uB9D0\uD558\uC9C0 \uC54A\uC558\uC73C\uBA74 \uD504\uB85C\uD544 \uACBD\uB825\uC73C\uB85C \uACC4\uC0B0\uD574 \uB123\uB418 \uD655\uC778. \uACBD\uB825 \uD45C\uAE30\uB97C \uC77D\uC744 \uC218 \uC5C6\uB294 \uACF5\uACE0\uB294 \uB0A8\uAE40')
|
|
126575
126592
|
},
|
|
126576
126593
|
// 공고를 가져오기만 한다. 처음 본 날짜 기록은 사용자 데이터가 아닌 로컬 캐시라 readOnly로 둔다.
|
|
126577
126594
|
readOnly: true,
|
|
@@ -126597,18 +126614,19 @@ ${retro_prompt}` : "");
|
|
|
126597
126614
|
rememberCategories,
|
|
126598
126615
|
saraminCategories,
|
|
126599
126616
|
postedSince: args.posted_since === "today" ? today : args.posted_since,
|
|
126617
|
+
years: args.years,
|
|
126600
126618
|
today
|
|
126601
126619
|
});
|
|
126602
126620
|
const failed = res.providers.filter((p) => !p.ok);
|
|
126603
126621
|
const okCount = res.results.length;
|
|
126604
126622
|
const parts = [`'${res.query || "\uC9C1\uBB34 \uC870\uAC74"}' \uAC80\uC0C9 \uACB0\uACFC ${okCount}\uAC74${usedSaved ? "(\uC800\uC7A5\uB41C \uAC80\uC0C9 \uC870\uAC74 \uC0AC\uC6A9)" : ""}`];
|
|
126605
|
-
if (res.filtered_out) parts.push(`(\uAC8C\uC2DC\uC77C \
|
|
126623
|
+
if (res.filtered_out) parts.push(`(\uAC8C\uC2DC\uC77C\xB7\uACBD\uB825 \uC870\uAC74\uC73C\uB85C ${res.filtered_out}\uAC74 \uC81C\uC678)`);
|
|
126606
126624
|
if (failed.length) parts.push(`(\uC77C\uBD80 \uC0AC\uC774\uD2B8 \uC751\uB2F5 \uC2E4\uD328: ${failed.map((f) => `${f.source}: ${f.error ?? f.status}`).join("; ")})`);
|
|
126607
126625
|
if (okCount === 0) parts.push("- \uD0A4\uC6CC\uB4DC\uB098 \uC9C1\uBB34 \uC870\uAC74\uC744 \uBC14\uAFB8\uAC70\uB098, \uACF5\uACE0 URL/\uBCF8\uBB38\uC744 \uC8FC\uC2DC\uBA74 \uC9C1\uC811 \uC800\uC7A5\uD574 \uB4DC\uB9B4\uAC8C\uC694.");
|
|
126608
126626
|
return ok(parts.join(" "), {
|
|
126609
126627
|
...res,
|
|
126610
126628
|
used_saved_preferences: usedSaved,
|
|
126611
|
-
conditions: { keyword, remember_categories: rememberCategories, saramin_categories: saraminCategories }
|
|
126629
|
+
conditions: { keyword, remember_categories: rememberCategories, saramin_categories: saraminCategories, years: args.years }
|
|
126612
126630
|
});
|
|
126613
126631
|
}
|
|
126614
126632
|
},
|
package/dist/web.mjs
CHANGED
|
@@ -55283,7 +55283,7 @@ var ApplicationContextSchema = external_exports.object({
|
|
|
55283
55283
|
});
|
|
55284
55284
|
|
|
55285
55285
|
// packages/shared/src/build-info.ts
|
|
55286
|
-
var APP_VERSION = true ? "0.0.
|
|
55286
|
+
var APP_VERSION = true ? "0.0.14" : "0.0.0-dev";
|
|
55287
55287
|
var BUNDLED = true;
|
|
55288
55288
|
|
|
55289
55289
|
// packages/shared/src/index.ts
|
package/docs/MCP_TOOLS.md
CHANGED
|
@@ -141,7 +141,7 @@ AI 클라이언트가 연결되면 이 도구를 가장 먼저 호출해 상태
|
|
|
141
141
|
|
|
142
142
|
| 도구 | 목적 | 주요 입력 | 반환 | 언제 호출 | 읽기전용 |
|
|
143
143
|
| --- | --- | --- | --- | --- | --- |
|
|
144
|
-
| `search_jobs` | 키워드나 직무 조건으로 무인증 공개 잡보드(원티드·점핏·리멤버·사람인)에서 공고를 *가져옴*(저장은 안 함). 여러 사이트의 같은 공고는 한 줄로 합침 | `keyword?`, `sources?`, `limit?`(사이트별, 기본 10, posted_since가 있으면 50, 최대 100), `remember_categories?`(["재무·회계·세무>회계결산"]), `saramin_categories?`(["322"]), `posted_since?`("today"·YYYY-MM-DD). keyword와 직무 조건을 모두 비우면 저장된 검색 조건 사용 | `{ query, results[](회사·직무·url·지역·경력·기술·마감·posted_at·first_seen_at·also_on, 사람인은 category_codes), providers[], filtered_out?, conditions, used_saved_preferences }` | "○○ 공고 찾아줘", "오늘 올라온 공고 보여줘". 결과를 보여주고 **고른 것만** save_job_posting. 원티드·점핏은 게시일이 없어 처음 본 날로 거름 | ✅ |
|
|
144
|
+
| `search_jobs` | 키워드나 직무 조건으로 무인증 공개 잡보드(원티드·점핏·리멤버·사람인)에서 공고를 *가져옴*(저장은 안 함). 여러 사이트의 같은 공고는 한 줄로 합침 | `keyword?`, `sources?`, `limit?`(사이트별, 기본 10, posted_since가 있으면 50, 최대 100), `remember_categories?`(["재무·회계·세무>회계결산"]), `saramin_categories?`(["322"]), `posted_since?`("today"·YYYY-MM-DD), `years?`(사용자 경력 연차, 공고 경력 범위에 들어가는 것만). keyword와 직무 조건을 모두 비우면 저장된 검색 조건 사용 | `{ query, results[](회사·직무·url·지역·경력·기술·마감·posted_at·first_seen_at·also_on, 사람인은 category_codes), providers[], filtered_out?, conditions, used_saved_preferences }` | "○○ 공고 찾아줘", "오늘 올라온 공고 보여줘". 결과를 보여주고 **고른 것만** save_job_posting. 원티드·점핏은 게시일이 없어 처음 본 날로 거름 | ✅ |
|
|
145
145
|
| `find_job_categories` | 직무 이름을 리멤버 분류("상위>하위")와 사람인 직무 코드로 변환(내장 분류표, 외부 통신 없음). 모든 직군 | `query`(쉼표로 여러 개), `limit?`(기본 20) | `{ query, remember[], saramin[](code·name·group·kind) }` 관련도순 | search_jobs·set_job_search_preferences에 직무 조건을 넣기 전 | ✅ |
|
|
146
146
|
| `get_job_search_preferences` | 저장된 공고 검색 조건 조회 | 없음 | `{ keyword, remember_categories, saramin_categories, updated_at }` | "내 검색 조건 뭐야?" | ✅ |
|
|
147
147
|
| `set_job_search_preferences` | 자주 볼 공고 검색 조건 저장(준 필드만 갱신). 리멤버 분류는 내장 분류표로 검증 | `keyword?`, `remember_categories?`, `saramin_categories?` | 저장된 조건 | 사용자 직무를 find_job_categories로 찾고 확인받은 뒤 | ✏️ |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "careermate",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CareerMate — an MCP-first, local-first career workspace. Your career database and documents stay on your machine; optional discovery and update features may use the internet without sending stored career data.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -50,7 +50,7 @@ export interface JobSearchResponse {
|
|
|
50
50
|
query: string;
|
|
51
51
|
results: JobSearchResult[];
|
|
52
52
|
providers: ProviderOutcome[];
|
|
53
|
-
/** Results dropped by `postedSince`. */
|
|
53
|
+
/** Results dropped by `postedSince` and `years`. */
|
|
54
54
|
filtered_out?: number;
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -64,6 +64,8 @@ export interface JobSearchOptions {
|
|
|
64
64
|
saraminCategories?: string[];
|
|
65
65
|
/** Keep only postings dated on/after this day (YYYY-MM-DD): posted_at, else first_seen_at. */
|
|
66
66
|
postedSince?: string;
|
|
67
|
+
/** User's years of experience: keep postings whose range covers it (unknown ranges stay). */
|
|
68
|
+
years?: number;
|
|
67
69
|
/** Record first-seen dates in the local DB (default true; tests turn it off). */
|
|
68
70
|
trackSeen?: boolean;
|
|
69
71
|
/** "Today" for first-seen/postedSince, injectable for tests. */
|
|
@@ -450,8 +452,13 @@ export async function searchJobs(query: string, opts: JobSearchOptions = {}): Pr
|
|
|
450
452
|
}
|
|
451
453
|
results = dedupeAcrossBoards(results);
|
|
452
454
|
let filtered_out: number | undefined;
|
|
453
|
-
|
|
454
|
-
|
|
455
|
+
const years = opts.years;
|
|
456
|
+
if (opts.postedSince || years != null) {
|
|
457
|
+
const kept = results.filter((r) => {
|
|
458
|
+
if (opts.postedSince && (r.posted_at ?? r.first_seen_at ?? today) < opts.postedSince) return false;
|
|
459
|
+
const range = years != null ? experienceRange(r.experience) : null;
|
|
460
|
+
return !range || (range[0] <= years! && years! <= range[1]);
|
|
461
|
+
});
|
|
455
462
|
filtered_out = results.length - kept.length;
|
|
456
463
|
results = kept;
|
|
457
464
|
}
|
|
@@ -464,6 +471,22 @@ export async function searchJobs(query: string, opts: JobSearchOptions = {}): Pr
|
|
|
464
471
|
};
|
|
465
472
|
}
|
|
466
473
|
|
|
474
|
+
/**
|
|
475
|
+
* Years a posting accepts, from each board's wording: "3~10년", "경력 3~10년", "5년 이상",
|
|
476
|
+
* "경력5년↑", "~3년", "신입", "신입·경력", "경력무관". Unreadable text gives null.
|
|
477
|
+
*/
|
|
478
|
+
export function experienceRange(text: string | undefined): [number, number] | null {
|
|
479
|
+
const t = (text ?? '').replace(/\s/g, '');
|
|
480
|
+
if (!t) return null;
|
|
481
|
+
if (/무관/.test(t)) return [0, Infinity];
|
|
482
|
+
const m = t.match(/(\d+)?~(\d+)년/);
|
|
483
|
+
if (m) return [m[1] ? +m[1] : 0, +m[2]];
|
|
484
|
+
const min = t.match(/(\d+)년(이상|↑)/);
|
|
485
|
+
if (min) return [/신입/.test(t) ? 0 : +min[1], Infinity];
|
|
486
|
+
if (/신입/.test(t)) return /경력/.test(t) ? [0, Infinity] : [0, 0];
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
|
|
467
490
|
// Any "(...)" goes: "(주)" and brand notes like "힐링페이퍼(강남언니)" vs "(주)힐링페이퍼".
|
|
468
491
|
const normCompany = (s: string) =>
|
|
469
492
|
s.replace(/\([^)]*\)|㈜|주식회사/g, '').toLowerCase().replace(/[^\p{L}\p{N}]/gu, '');
|
|
@@ -1110,6 +1110,13 @@ export const TOOLS: ToolDef[] = [
|
|
|
1110
1110
|
.refine((s) => s === 'today' || isYmd(s), '"today" 또는 실제 날짜(YYYY-MM-DD)')
|
|
1111
1111
|
.optional()
|
|
1112
1112
|
.describe('"today" 또는 YYYY-MM-DD. 그날 이후 공고만'),
|
|
1113
|
+
years: z
|
|
1114
|
+
.number()
|
|
1115
|
+
.int()
|
|
1116
|
+
.min(0)
|
|
1117
|
+
.max(40)
|
|
1118
|
+
.optional()
|
|
1119
|
+
.describe('사용자 경력 연차(신입 0). 공고의 경력 범위에 이 연차가 들어가는 것만 남김. "5년차"면 5. 사용자가 말하지 않았으면 프로필 경력으로 계산해 넣되 확인. 경력 표기를 읽을 수 없는 공고는 남김'),
|
|
1113
1120
|
},
|
|
1114
1121
|
// 공고를 가져오기만 한다. 처음 본 날짜 기록은 사용자 데이터가 아닌 로컬 캐시라 readOnly로 둔다.
|
|
1115
1122
|
readOnly: true,
|
|
@@ -1135,18 +1142,19 @@ export const TOOLS: ToolDef[] = [
|
|
|
1135
1142
|
rememberCategories,
|
|
1136
1143
|
saraminCategories,
|
|
1137
1144
|
postedSince: args.posted_since === 'today' ? today : args.posted_since,
|
|
1145
|
+
years: args.years,
|
|
1138
1146
|
today,
|
|
1139
1147
|
});
|
|
1140
1148
|
const failed = res.providers.filter((p) => !p.ok);
|
|
1141
1149
|
const okCount = res.results.length;
|
|
1142
1150
|
const parts = [`'${res.query || '직무 조건'}' 검색 결과 ${okCount}건${usedSaved ? '(저장된 검색 조건 사용)' : ''}`];
|
|
1143
|
-
if (res.filtered_out) parts.push(`(
|
|
1151
|
+
if (res.filtered_out) parts.push(`(게시일·경력 조건으로 ${res.filtered_out}건 제외)`);
|
|
1144
1152
|
if (failed.length) parts.push(`(일부 사이트 응답 실패: ${failed.map((f) => `${f.source}: ${f.error ?? f.status}`).join('; ')})`);
|
|
1145
1153
|
if (okCount === 0) parts.push('- 키워드나 직무 조건을 바꾸거나, 공고 URL/본문을 주시면 직접 저장해 드릴게요.');
|
|
1146
1154
|
return ok(parts.join(' '), {
|
|
1147
1155
|
...res,
|
|
1148
1156
|
used_saved_preferences: usedSaved,
|
|
1149
|
-
conditions: { keyword, remember_categories: rememberCategories, saramin_categories: saraminCategories },
|
|
1157
|
+
conditions: { keyword, remember_categories: rememberCategories, saramin_categories: saraminCategories, years: args.years },
|
|
1150
1158
|
});
|
|
1151
1159
|
},
|
|
1152
1160
|
},
|