careermate 0.0.8 → 0.0.9
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 +2 -2
- package/LICENSE +1 -1
- package/README.md +78 -81
- package/bin/careermate.mjs +11 -2
- package/dist/career-os/eop/cover-letter.md +10 -4
- package/dist/career-os/eop/interview-prep.md +1 -0
- package/dist/career-os/knowledge/cover-letter.md +4 -0
- package/dist/career-os/knowledge/verifiers/human-voice.md +7 -2
- package/dist/doctor.mjs +888 -340
- package/dist/mcp.mjs +904 -350
- package/dist/migrate.mjs +208 -16
- package/dist/public/i18n/en/applications.js +19 -0
- package/dist/public/i18n/en/common.js +6 -0
- package/dist/public/i18n/en/jobs.js +17 -0
- package/dist/public/i18n/en/settings.js +5 -2
- package/dist/public/i18n/ko/applications.js +19 -0
- package/dist/public/i18n/ko/common.js +5 -0
- package/dist/public/i18n/ko/jobs.js +17 -0
- package/dist/public/i18n/ko/settings.js +5 -2
- package/dist/public/lib.js +29 -0
- package/dist/public/pages/applications.js +86 -10
- package/dist/public/pages/documents.js +16 -7
- package/dist/public/pages/home.js +12 -0
- package/dist/public/pages/interview.js +11 -4
- package/dist/public/pages/jobs.js +71 -1
- package/dist/public/pages/settings.js +2 -2
- package/dist/public/styles.css +13 -0
- package/dist/web.mjs +544 -199
- package/docs/FAQ.md +13 -3
- package/docs/MCP_TOOLS.md +22 -9
- package/docs/career-os/eop/cover-letter.md +10 -4
- package/docs/career-os/eop/interview-prep.md +1 -0
- package/docs/career-os/knowledge/cover-letter.md +4 -0
- package/docs/career-os/knowledge/verifiers/human-voice.md +7 -2
- package/package.json +3 -1
- package/packages/core/src/index.ts +1 -0
- package/packages/core/src/jobsearch.ts +6 -2
- package/packages/core/src/offers.ts +41 -0
- package/packages/core/src/services.ts +5 -1
- package/packages/core/src/summary.ts +245 -19
- package/packages/db/src/connection.ts +70 -2
- package/packages/db/src/index.ts +1 -0
- package/packages/db/src/repositories.ts +140 -8
- package/packages/db/src/schema.ts +140 -8
- package/packages/mcp-tools/src/result.ts +9 -0
- package/packages/mcp-tools/src/tools.ts +143 -20
- package/packages/parsers/src/documents.ts +7 -1
- package/packages/prompts/src/humanize.ts +4 -0
- package/packages/shared/src/schemas.ts +134 -0
- package/scripts/init.ts +2 -0
|
@@ -23,9 +23,12 @@ import {
|
|
|
23
23
|
CoverLetterVersionInputSchema,
|
|
24
24
|
ApplicationSubmissionSchema,
|
|
25
25
|
InterviewPrepInputSchema,
|
|
26
|
+
OfferInputSchema,
|
|
26
27
|
DocumentInputSchema,
|
|
27
28
|
APPLICATION_STATUSES,
|
|
28
29
|
APPLICATION_STATUS_LABELS,
|
|
30
|
+
REJECTED_STAGES,
|
|
31
|
+
REJECTED_STAGE_LABELS,
|
|
29
32
|
DOCUMENT_KINDS,
|
|
30
33
|
INTERVIEW_UNLOCK_STATUSES,
|
|
31
34
|
} from '@careermate/shared';
|
|
@@ -65,6 +68,9 @@ import {
|
|
|
65
68
|
prescreenJob,
|
|
66
69
|
searchJobs,
|
|
67
70
|
JOB_SEARCH_SOURCES,
|
|
71
|
+
saveOffer,
|
|
72
|
+
compareOffers,
|
|
73
|
+
getOffer,
|
|
68
74
|
jobWithMeta,
|
|
69
75
|
previewCoverLetter,
|
|
70
76
|
summarizeReport,
|
|
@@ -357,6 +363,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
357
363
|
},
|
|
358
364
|
{
|
|
359
365
|
name: 'save_profile',
|
|
366
|
+
idempotent: true, // 부분 업서트 — 같은 인자 재호출은 같은 결과
|
|
360
367
|
title: '프로필 저장',
|
|
361
368
|
description:
|
|
362
369
|
'사용자 프로필을 저장합니다. 전달한 필드만 갱신되고 나머지는 유지됩니다(부분 저장 안전). 이력서를 구조화한 뒤 이름/한 줄 소개/희망 직무/선호 문체/강조 포인트 등을 채워 저장하세요. 선호 문체(preferred_tone)와 강조 포인트(emphasis_points)는 이후 자기소개서 작성 품질에 직접 영향을 줍니다. 학력(education)·자격증(certifications)·어학점수(language_scores: 토익·오픽 등)·수상(awards)이 이력서에 있으면 구조화해 저장하세요 — 한국 이력서 핵심 스펙이고, 저장하면 이후 자소서·이력서에서 정확한 수치(학점 3.8, 토익 920)를 지어내지 않고 그대로 인용합니다.',
|
|
@@ -398,6 +405,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
398
405
|
},
|
|
399
406
|
{
|
|
400
407
|
name: 'delete_resume',
|
|
408
|
+
destructive: true,
|
|
401
409
|
title: '이력서/문서 삭제',
|
|
402
410
|
description:
|
|
403
411
|
'저장된 이력서·경력기술서·포트폴리오 등 문서를 삭제합니다. 사용자가 명확히 삭제를 요청했을 때만 사용하세요. 먼저 get_resumes로 목록을 확인하고 어떤 문서인지 사용자와 확인한 뒤 호출합니다. confirm에는 DELETE를 넣으세요.',
|
|
@@ -418,6 +426,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
418
426
|
/* ----------------------------------------------------------- experiences */
|
|
419
427
|
{
|
|
420
428
|
name: 'add_experience',
|
|
429
|
+
idempotent: true, // 회사·직무·입사일 키 업서트(adr_0003) — 재호출 안전
|
|
421
430
|
title: '경력 추가',
|
|
422
431
|
description:
|
|
423
432
|
'구조화된 경력(직장 단위)을 저장합니다. 이력서에서 파악한 경력을 experiences 배열에 한 번에 담아 호출하세요(한 건이어도 배열로 — 항목마다 도구를 반복 호출하지 마세요). 이력서 본문(add_resume)과 달리 회사·직무·기간·성과를 필드로 나눠 저장해 적합도 분석/자소서 작성 시 정밀하게 활용됩니다. 각 항목은 company가 필수이며, achievements에는 정량 지표가 담긴 성과를, tech에는 사용 기술을 배열로 넣으세요. 같은 회사·직무·입사일(start_date)이 일치하면 갱신되고 중복 생성되지 않으니 여러 번 저장해도 안전합니다. 온보딩 완성도(get_onboarding_status)의 has_experience를 채우려면 1건 이상 추가하세요.',
|
|
@@ -443,6 +452,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
443
452
|
/* -------------------------------------------------------------- projects */
|
|
444
453
|
{
|
|
445
454
|
name: 'add_project',
|
|
455
|
+
idempotent: true, // 이름 키 업서트 — 재호출 안전
|
|
446
456
|
title: '프로젝트 추가',
|
|
447
457
|
description:
|
|
448
458
|
'대표 프로젝트를 저장합니다. projects 배열에 여러 개를 한 번에 담아 호출하세요(한 개여도 배열로 — 항목마다 반복 호출하지 마세요). 각 항목은 name이 필수이며, highlights(핵심 성과)와 tech(사용 기술)를 배열로 넣으면 자소서·면접 준비에서 근거로 활용됩니다. url에 결과물 링크를 넣을 수 있습니다. 같은 이름의 프로젝트는 갱신되고 중복 생성되지 않으니 여러 번 저장해도 안전합니다.',
|
|
@@ -468,6 +478,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
468
478
|
/* ---------------------------------------------------------------- skills */
|
|
469
479
|
{
|
|
470
480
|
name: 'add_skill',
|
|
481
|
+
idempotent: true, // 이름 키 업서트 — 재호출 안전
|
|
471
482
|
title: '기술스택 추가',
|
|
472
483
|
description:
|
|
473
484
|
'보유 기술스택을 저장합니다. 여러 개를 skills 배열에 한 번에 담아 호출하세요(한 개여도 배열로 — 기술마다 도구를 반복 호출하지 마세요). 각 항목은 name이 필수이며, category(언어/프레임워크/툴/소프트스킬 등)·level·years로 숙련도를 함께 기록하면 공고 키워드 매칭이 정확해집니다. 같은 이름의 기술은 갱신되고 중복 생성되지 않으니 여러 번 저장해도 안전합니다. 온보딩 완성도의 has_skills를 채우려면 1개 이상 추가하세요.',
|
|
@@ -491,6 +502,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
491
502
|
},
|
|
492
503
|
{
|
|
493
504
|
name: 'delete_experience',
|
|
505
|
+
destructive: true,
|
|
494
506
|
title: '경력 항목 삭제',
|
|
495
507
|
description:
|
|
496
508
|
'구조화된 경력(직장) 항목 하나를 삭제합니다. 잘못 추출/입력된 경력을 지울 때 사용하세요. 먼저 get_experiences로 목록과 id를 확인하고 사용자와 확인한 뒤 호출합니다. (내용 수정은 add_experience로 같은 항목을 다시 저장하면 갱신됩니다.) confirm에는 DELETE를 넣으세요.',
|
|
@@ -509,6 +521,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
509
521
|
},
|
|
510
522
|
{
|
|
511
523
|
name: 'delete_project',
|
|
524
|
+
destructive: true,
|
|
512
525
|
title: '프로젝트 항목 삭제',
|
|
513
526
|
description:
|
|
514
527
|
'구조화된 프로젝트 항목 하나를 삭제합니다. 잘못 추출/입력된 프로젝트를 지울 때 사용하세요. 먼저 get_projects로 목록과 id를 확인하고 사용자와 확인한 뒤 호출합니다. (내용 수정은 add_project로 같은 항목을 다시 저장하면 갱신됩니다.) confirm에는 DELETE를 넣으세요.',
|
|
@@ -527,6 +540,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
527
540
|
},
|
|
528
541
|
{
|
|
529
542
|
name: 'delete_skill',
|
|
543
|
+
destructive: true,
|
|
530
544
|
title: '기술스택 항목 삭제',
|
|
531
545
|
description:
|
|
532
546
|
'구조화된 기술스택 항목 하나를 삭제합니다. 잘못 추출/입력된 기술을 지울 때 사용하세요. 먼저 get_skills로 목록과 id를 확인하고 사용자와 확인한 뒤 호출합니다. (내용 수정은 add_skill로 같은 항목을 다시 저장하면 갱신됩니다.) confirm에는 DELETE를 넣으세요.',
|
|
@@ -584,6 +598,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
584
598
|
},
|
|
585
599
|
{
|
|
586
600
|
name: 'delete_cover_letter',
|
|
601
|
+
destructive: true,
|
|
587
602
|
title: '자기소개서 삭제',
|
|
588
603
|
description:
|
|
589
604
|
'사용자가 저장된 자기소개서 삭제를 명확히 요청했을 때만 사용합니다. 먼저 get_cover_letters로 전체 목록 또는 공고별 목록을 확인하고, 삭제할 항목을 사용자가 확인한 뒤 호출하세요. 삭제하면 해당 자기소개서의 모든 버전이 함께 삭제되며 되돌릴 수 없습니다. confirm에는 DELETE를 넣으세요.',
|
|
@@ -612,9 +627,10 @@ export const TOOLS: ToolDef[] = [
|
|
|
612
627
|
/* ------------------------------------------------------------------ jobs */
|
|
613
628
|
{
|
|
614
629
|
name: 'save_job_posting',
|
|
630
|
+
idempotent: true, // 같은 url 업서트 — 재호출 안전
|
|
615
631
|
title: '채용공고 저장',
|
|
616
632
|
description:
|
|
617
|
-
'채용공고를 저장합니다(있으면 갱신). company와 position은 필수입니다. 공고 원문은 description에, 핵심 자격요건/우대사항은 requirements 배열에, 핵심 키워드는 keywords에 정리해 넣으면 이후 적합도 분석/자소서 작성에 활용됩니다. 회사를 리서치했다면 사업개요·미션은 company_overview, 회사가 강조하는 인재상은 talent_profile, 핵심가치는 core_values에 넣으세요 — 한국 자소서·면접은 인재상/핵심가치 정렬이 핵심이라 저장해두면 이후 작성·면접 준비에서 그대로 끌어다 씁니다. 같은 url이면 중복 생성 없이 갱신됩니다. 저장 시 지원(application) 항목이 자동으로 생성됩니다.',
|
|
633
|
+
'채용공고를 저장합니다(있으면 갱신). company와 position은 필수입니다. 공고 원문은 description에, 핵심 자격요건/우대사항은 requirements 배열에, 핵심 키워드는 keywords에 정리해 넣으면 이후 적합도 분석/자소서 작성에 활용됩니다. 회사를 리서치했다면 사업개요·미션은 company_overview, 회사가 강조하는 인재상은 talent_profile, 핵심가치는 core_values에 넣으세요 — 한국 자소서·면접은 인재상/핵심가치 정렬이 핵심이라 저장해두면 이후 작성·면접 준비에서 그대로 끌어다 씁니다. 회사 평판(평점·워라밸·문화 리스크 등)을 리서치했다면 reputation에 요약(출처·최근성 표기)을 넣으면 오퍼 판단·면접에 활용됩니다. 회사 지정 자소서 문항·글자수가 있으면 questions 배열에 구조화 저장하세요(question 필수, max_chars/min_chars/count_mode). 이후 자소서 작성 시 문항별로 validate_cover_letter(min_chars/max_chars/count_mode)로 한도를 점검합니다. 면접·인적성·발표 등 전형 일정을 받으면 schedule 배열에 구조화 저장하세요(label·date 필수, time 선택; 서류 마감은 deadline 필드). 면접초대 메일을 사용자가 붙여넣으면 날짜·시간을 추출해 저장하고, 같은 날 다른 일정과 겹치면 알려주세요. 같은 url이면 중복 생성 없이 갱신됩니다. 저장 시 지원(application) 항목이 자동으로 생성됩니다.',
|
|
618
634
|
inputSchema: JobInputSchema.shape,
|
|
619
635
|
handler: (args) => {
|
|
620
636
|
const { job } = saveJobPosting(args);
|
|
@@ -660,6 +676,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
660
676
|
},
|
|
661
677
|
{
|
|
662
678
|
name: 'delete_job_posting',
|
|
679
|
+
destructive: true,
|
|
663
680
|
title: '채용공고 삭제',
|
|
664
681
|
description:
|
|
665
682
|
'사용자가 저장된 채용공고 삭제를 명확히 요청했을 때만 사용합니다. 먼저 list_jobs로 전체 공고 목록을 확인하고, 삭제할 공고를 사용자가 확인한 뒤 호출하세요. 삭제하면 연결된 지원 상태·적합도 분석·면접 준비가 함께 삭제됩니다. 연결된 자기소개서는 삭제하지 않고 공고 연결만 해제합니다. confirm에는 DELETE를 넣으세요.',
|
|
@@ -760,6 +777,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
760
777
|
},
|
|
761
778
|
{
|
|
762
779
|
name: 'save_fit_analysis',
|
|
780
|
+
idempotent: true, // 공고당 1건 업서트
|
|
763
781
|
title: '적합도 분석 결과 저장',
|
|
764
782
|
description:
|
|
765
783
|
'공고와 사용자 정보를 비교해 당신(AI)이 도출한 적합도 분석을 저장합니다. job_id는 필수이며 먼저 save_job_posting으로 공고가 저장되어 있어야 합니다. score(0~100), summary, strengths(강점), gaps(보완 필요), matched/missing_keywords, recommendations(자소서·지원 전략 제안)를 채워 저장하세요. 같은 공고에 다시 저장하면 갱신됩니다.',
|
|
@@ -806,21 +824,36 @@ export const TOOLS: ToolDef[] = [
|
|
|
806
824
|
{
|
|
807
825
|
name: 'update_application_status',
|
|
808
826
|
title: '지원 상태 변경',
|
|
809
|
-
description:
|
|
827
|
+
description:
|
|
828
|
+
`지원 상태를 변경합니다. 가능한 상태: ${STATUS_LIST}. '서류 합격(document_passed)' 이상으로 바뀌면 면접 준비를 제안하는 힌트를 함께 돌려줍니다. note에 변경 사유를 남길 수 있습니다. ` +
|
|
829
|
+
"'불합격(rejected)'으로 바꿀 때는 rejected_stage에 어느 단계에서 아쉽게 됐는지 함께 남기세요 — 서류=document, 인적성·AI역검=assessment, 면접=interview, 최종·처우=final, 기타=other. 탈락 사유를 알려주는 기업은 드물어, 이 자가 기록이 쌓이면 '서류는 붙는데 면접에서 떨어진다' 같은 패턴이 로컬 데이터로 드러납니다(get_pipeline_stats).",
|
|
810
830
|
inputSchema: {
|
|
811
831
|
job_id: z.string(),
|
|
812
832
|
status: z.enum(APPLICATION_STATUSES),
|
|
813
833
|
note: z.string().optional(),
|
|
814
834
|
submission: ApplicationSubmissionSchema.optional(),
|
|
835
|
+
rejected_stage: z
|
|
836
|
+
.enum(REJECTED_STAGES)
|
|
837
|
+
.optional()
|
|
838
|
+
.describe(
|
|
839
|
+
"탈락 회고 — 'rejected'로 바꿀 때 어느 단계에서 탈락했는지: 서류=document, 인적성·AI역검=assessment, 면접=interview, 최종·처우=final, 기타=other",
|
|
840
|
+
),
|
|
815
841
|
},
|
|
816
842
|
handler: (args) => {
|
|
817
843
|
try {
|
|
818
|
-
const res = updateApplicationStatus(args.job_id, args.status, args.note, args.submission);
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
844
|
+
const res = updateApplicationStatus(args.job_id, args.status, args.note, args.submission, args.rejected_stage);
|
|
845
|
+
// 탈락으로 바꿨는데 단계가 안 남았으면: 실패시키지 않고 저장은 하되, 회고를 유도한다.
|
|
846
|
+
const needsRetro = args.status === 'rejected' && !res.application.rejected_stage;
|
|
847
|
+
const retro_prompt =
|
|
848
|
+
'어느 단계에서 아쉽게 됐는지(서류/인적성/면접/최종)와 배운 점을 남겨두면 다음 지원 전략에 쓸 수 있어요. 기록할까요?';
|
|
849
|
+
const base =
|
|
850
|
+
`지원 상태를 '${APPLICATION_STATUS_LABELS[args.status as keyof typeof APPLICATION_STATUS_LABELS]}'(으)로 바꿨어요.`;
|
|
851
|
+
const user_message = base + (res.hint ? `\n\n${res.hint}` : needsRetro ? `\n\n${retro_prompt}` : '');
|
|
852
|
+
const text = needsRetro
|
|
853
|
+
? `${user_message}\n\n(AI 안내: 사용자에게 어느 단계에서 떨어졌는지와 배운 점을 물어, rejected_stage와 note로 함께 기록하도록 제안하세요.)`
|
|
854
|
+
: user_message;
|
|
822
855
|
return ok(
|
|
823
|
-
|
|
856
|
+
text,
|
|
824
857
|
{
|
|
825
858
|
...res,
|
|
826
859
|
user_message,
|
|
@@ -829,7 +862,12 @@ export const TOOLS: ToolDef[] = [
|
|
|
829
862
|
prompt: res.hint,
|
|
830
863
|
next_workflow: 'prepare_interview',
|
|
831
864
|
}
|
|
832
|
-
:
|
|
865
|
+
: needsRetro
|
|
866
|
+
? {
|
|
867
|
+
prompt: retro_prompt,
|
|
868
|
+
next_tool: 'update_application_status',
|
|
869
|
+
}
|
|
870
|
+
: null,
|
|
833
871
|
},
|
|
834
872
|
);
|
|
835
873
|
} catch (e) {
|
|
@@ -841,6 +879,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
841
879
|
/* ---------------------------------------------------------- interview prep */
|
|
842
880
|
{
|
|
843
881
|
name: 'save_interview_prep',
|
|
882
|
+
idempotent: true, // 공고당 1건 업서트(debrief는 병합)
|
|
844
883
|
title: '면접 준비 자료 저장',
|
|
845
884
|
description:
|
|
846
885
|
'예상 면접 질문, 꼬리 질문, STAR 답변 가이드, 1분 자기소개 초안 등 해당 공고 기준 면접 준비 자료를 저장합니다. 공고/직무/자기소개서를 근거로 당신(AI)이 생성한 자료를 저장하는 데 사용합니다. ' +
|
|
@@ -1003,15 +1042,17 @@ export const TOOLS: ToolDef[] = [
|
|
|
1003
1042
|
name: 'get_followups',
|
|
1004
1043
|
title: '후속 조치 알림(놓친 지원·마감·면접준비)',
|
|
1005
1044
|
description:
|
|
1006
|
-
'지금 챙겨야 할 것들을 한 번에 돌려줍니다: ① 응답 없이 오래된 지원(지원 후 무응답·서류합격 후 일정대기·면접 후 결과대기) ② 임박했거나 지난 지원 마감(지원 전 단계) ③ 합격했는데 아직 면접 준비가 없는
|
|
1045
|
+
'지금 챙겨야 할 것들을 한 번에 돌려줍니다: ① 응답 없이 오래된 지원(지원 후 무응답·서류합격 후 일정대기·면접 후 결과대기) ② 임박했거나 지난 지원 마감(지원 전 단계) ③ 합격했는데 아직 면접 준비가 없는 공고 ④ 오늘~7일 내 다가오는 전형 일정(면접·인적성·발표; `upcoming_schedule`에 각 일정의 D-day) ⑤ 서로 다른 공고의 일정이 같은 날짜에 겹칠 때(`schedule_conflicts` — 같은 공고의 연속 단계는 충돌이 아니고, 서로 다른 공고 2건 이상이 한 날에 몰릴 때만; 있으면 사용자에게 알려 조정 제안). 사용자가 "내 지원 현황 어때?", "뭐 챙겨야 해?", "이번 주 일정 뭐 있어?", "후속 연락할 거 있어?"라고 묻거나, 세션을 이어가며 놓친 게 없는지 먼저 점검할 때 사용하세요. 읽기 전용입니다. 지원(applied) 항목에는 결정론 팔로업 타이밍이 붙습니다: `followup_timing="due"`는 지원 후 5~7영업일(주말 제외)로 정중한 후속 문의 적기, `"overdue"`는 8영업일 이상으로 적기가 지났다는 뜻이며, `business_days_since`는 경과 영업일 수입니다. 달력일 45일 이상 무응답이면 `suggest_on_hold=true`가 붙으니 "보류(on_hold)로 옮길까요?"라고 제안하세요. 응답의 reason은 사용자 언어로 풀어 전하고, 후속 문의는 강요하지 말고 선택지로 제안하세요(채용 관행상 과한 독촉은 역효과 — 문의는 정중히 1회).',
|
|
1007
1046
|
inputSchema: {},
|
|
1008
1047
|
readOnly: true,
|
|
1009
1048
|
handler: () => {
|
|
1010
1049
|
const d = getActionDigest();
|
|
1011
|
-
const n = d.followups.length + d.deadlines.length + d.interview_todo.length;
|
|
1012
|
-
const
|
|
1013
|
-
|
|
1014
|
-
|
|
1050
|
+
const n = d.followups.length + d.deadlines.length + d.interview_todo.length + d.upcoming_schedule.length;
|
|
1051
|
+
const parts = [`후속 ${d.followups.length}`, `마감 ${d.deadlines.length}`, `면접준비 ${d.interview_todo.length}`, `일정 ${d.upcoming_schedule.length}`];
|
|
1052
|
+
if (d.schedule_conflicts.length) parts.push(`일정충돌 ${d.schedule_conflicts.length}`);
|
|
1053
|
+
const msg = n === 0 && d.schedule_conflicts.length === 0
|
|
1054
|
+
? '지금 당장 챙길 후속 항목이 없습니다(무응답 지원·임박 마감·면접 준비 대기·다가오는 일정 없음).'
|
|
1055
|
+
: `챙길 항목: ${parts.join(' · ')}`;
|
|
1015
1056
|
return ok(msg, d);
|
|
1016
1057
|
},
|
|
1017
1058
|
},
|
|
@@ -1019,7 +1060,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
1019
1060
|
name: 'get_pipeline_stats',
|
|
1020
1061
|
title: '지원 파이프라인 분석(깔때기·합격률)',
|
|
1021
1062
|
description:
|
|
1022
|
-
'전체 지원 현황을 분석해 돌려줍니다: 현재 상태 분포(by_status), 단계별 도달 깔때기(funnel: 지원→서류합격→면접→최종, 상태 변경 이력으로 복원한 실측치라 현재 불합격이어도 거쳐간 최고 단계까지 집계), 통과율(rates: 서류·면접·최종, 지원 대비 %), 결과 요약(outcomes). 사용자가 "내 합격률 어때?", "지원 통계 보여줘", "어느 단계에서 많이 떨어져?"라고 물을 때 사용하세요. 비율은 설명용이며 표본이 작으면 예측력이 낮다는 점을 함께 전하세요(note 참고). 읽기 전용입니다.',
|
|
1063
|
+
'전체 지원 현황을 분석해 돌려줍니다: 현재 상태 분포(by_status), 단계별 도달 깔때기(funnel: 지원→서류합격→면접→최종, 상태 변경 이력으로 복원한 실측치라 현재 불합격이어도 거쳐간 최고 단계까지 집계), 통과율(rates: 서류·면접·최종, 지원 대비 %), 결과 요약(outcomes), 탈락 단계 분포(rejected_by_stage: 서류·인적성·면접·최종·기타별 탈락 건수 + rejected_unrecorded: 단계 미기록 수). 사용자가 "내 합격률 어때?", "지원 통계 보여줘", "어느 단계에서 많이 떨어져?"라고 물을 때 사용하세요. 비율은 설명용이며 표본이 작으면 예측력이 낮다는 점을 함께 전하세요(note 참고). 읽기 전용입니다.',
|
|
1023
1064
|
inputSchema: {},
|
|
1024
1065
|
readOnly: true,
|
|
1025
1066
|
handler: () => {
|
|
@@ -1027,14 +1068,20 @@ export const TOOLS: ToolDef[] = [
|
|
|
1027
1068
|
if (s.total === 0) return ok('아직 지원 내역이 없어 분석할 통계가 없습니다.', s);
|
|
1028
1069
|
const r = s.rates;
|
|
1029
1070
|
const fmt = (v: number | null) => (v == null ? '—' : `${v}%`);
|
|
1071
|
+
// 탈락 분포 한 줄: 기록된 단계별 건수 + 미기록이 있으면 기록 유도.
|
|
1072
|
+
const rejParts = s.rejected_by_stage.map((x) => `${REJECTED_STAGE_LABELS[x.stage]} ${x.count}`);
|
|
1073
|
+
if (s.rejected_unrecorded > 0) rejParts.push(`미기록 ${s.rejected_unrecorded}건 — 기록하면 패턴이 보입니다`);
|
|
1074
|
+
const rejLine = rejParts.length ? ` · 탈락 분포: ${rejParts.join('·')}` : '';
|
|
1030
1075
|
return ok(
|
|
1031
|
-
`지원 ${s.total}건 · 깔때기 지원 ${s.funnel.applied}→서류 ${s.funnel.document_passed}→면접 ${s.funnel.interview}→최종 ${s.funnel.final_passed} · 통과율 서류 ${fmt(r.document_pass)}·면접 ${fmt(r.interview)}·최종 ${fmt(r.offer)} (표본 작으면 추세 참고로만)`,
|
|
1076
|
+
`지원 ${s.total}건 · 깔때기 지원 ${s.funnel.applied}→서류 ${s.funnel.document_passed}→면접 ${s.funnel.interview}→최종 ${s.funnel.final_passed} · 통과율 서류 ${fmt(r.document_pass)}·면접 ${fmt(r.interview)}·최종 ${fmt(r.offer)}${rejLine} (표본 작으면 추세 참고로만)`,
|
|
1032
1077
|
s,
|
|
1033
1078
|
);
|
|
1034
1079
|
},
|
|
1035
1080
|
},
|
|
1036
1081
|
{
|
|
1037
1082
|
name: 'search_jobs',
|
|
1083
|
+
// 외부 공개 잡보드(원티드·점핏)로 검색어가 나간다 — openWorld를 정직하게 표시.
|
|
1084
|
+
openWorld: true,
|
|
1038
1085
|
title: '공고 검색·가져오기(키워드)',
|
|
1039
1086
|
description:
|
|
1040
1087
|
`키워드로 무인증 공개 채용 사이트(${JOB_SEARCH_SOURCES.join('·')})에서 채용 공고를 가져옵니다. 사용자가 "백엔드 신입 공고 찾아줘", "React 공고 가져와"처럼 공고를 찾아 달라고 할 때 사용하세요. 결과(회사·직무·링크·지역·경력·기술스택·마감)를 사용자에게 보기 좋게 보여 주고, **사용자가 관심 있다고 고른 것만** save_job_posting으로 저장하세요(전부 자동 저장하지 마세요). 저장한 공고는 이어서 get_application_context로 분석·자소서로 연결됩니다. ` +
|
|
@@ -1059,16 +1106,86 @@ export const TOOLS: ToolDef[] = [
|
|
|
1059
1106
|
name: 'prescreen_job',
|
|
1060
1107
|
title: '공고 사전 프리스크린(키워드 겹침)',
|
|
1061
1108
|
description:
|
|
1062
|
-
'저장된 공고의 핵심 키워드를
|
|
1109
|
+
'저장된 공고의 핵심 키워드를 **사용자의 보유 기술·프로젝트/경력 기술스택(구조화 스킬)과 대표 이력서 본문 양쪽**에 대조해 "이 공고를 깊게 볼 가치가 있는지"를 빠르게 가늠합니다. LLM 없이 셀 수 있는 키워드 겹침만 돌려주는 값싼 사전 필터입니다: `matched`(스킬 목록에서 발견)·`resume_matched`(이력서 본문에서 발견)·`combined_matched`(둘의 합집합)·`combined_coverage`(합집합 커버리지 0~1)·`resume_checked`(대표 이력서 존재 여부). **진짜 적합도 판단은 당신(AI)이 get_application_context로 맥락을 받아 직접 수행**합니다(이 도구는 그 대체가 아닙니다). 사용자가 공고를 여러 개 훑으며 우선순위를 정하거나 "이거 지원할 만해?"라고 물을 때 본격 분석 전에 가볍게 호출하세요. **combined_coverage가 낮은데 실제로는 보유한 역량이라면, 이력서 본문에 그 키워드가 안 드러나는 것이니 이력서 보강(해당 키워드 노출)을 제안하세요**(ATS 키워드 매칭에 유효). coverage가 낮아도 키워드 미기재일 수 있으니 단정하지 말고, 표기되지 않은 역량은 사용자에게 확인하세요. 읽기 전용입니다.',
|
|
1063
1110
|
inputSchema: { job_id: z.string() },
|
|
1064
1111
|
readOnly: true,
|
|
1065
1112
|
handler: (args) => {
|
|
1066
1113
|
const r = prescreenJob(args.job_id);
|
|
1067
1114
|
if (!r) return fail('공고를 찾을 수 없습니다. 먼저 save_job_posting으로 저장하세요.');
|
|
1068
|
-
if (r.
|
|
1115
|
+
if (r.combined_coverage == null)
|
|
1069
1116
|
return ok('이 공고에 저장된 핵심 키워드가 없어 겹침을 셀 수 없습니다. save_job_posting의 keywords에 핵심 키워드를 정리해 넣으면 프리스크린이 가능합니다.', r);
|
|
1070
|
-
const pct = Math.round(r.
|
|
1071
|
-
|
|
1117
|
+
const pct = Math.round(r.combined_coverage * 100);
|
|
1118
|
+
const combinedMissing = r.job_keywords.filter((k) => !r.combined_matched.includes(k));
|
|
1119
|
+
const resumeNote = r.resume_checked
|
|
1120
|
+
? ''
|
|
1121
|
+
: ' (대표 이력서가 없어 스킬 목록만 대조했어요 — 이력서를 저장하면 본문 키워드까지 대조합니다.)';
|
|
1122
|
+
return ok(
|
|
1123
|
+
`키워드 커버리지 ${pct}% — 스킬+대표 이력서 본문에서 ${r.combined_matched.length}/${r.job_keywords.length}개 확인.${resumeNote} 아직 안 드러난 키워드: ${combinedMissing.join(', ') || '없음'}. (셀 수 있는 겹침일 뿐 실제 적합도는 직접 판단하세요. 보유 역량인데 안 드러났다면 이력서 보강을 제안하세요.)`,
|
|
1124
|
+
r,
|
|
1125
|
+
);
|
|
1126
|
+
},
|
|
1127
|
+
},
|
|
1128
|
+
{
|
|
1129
|
+
name: 'save_offer',
|
|
1130
|
+
idempotent: true, // 공고당 1건 업서트
|
|
1131
|
+
title: '오퍼(제안) 저장',
|
|
1132
|
+
description:
|
|
1133
|
+
'받은 채용 제안(오퍼)을 공고에 연결해 저장합니다(공고당 1건, 다시 저장하면 갱신). 현금성 항목은 **만원 단위 숫자**로 — base_salary(기본급/연봉)·bonus_amount(연 성과급·상여 기대액)·welfare_amount(복지포인트 등 연환산)·signing_amount(사이닝 일시금). 주식/스톡옵션은 가치평가가 모호하니 equity_note에 텍스트로, 그 외 보상은 comp_note에. 근무형태·계약형태·수락 마감일(accept_deadline)도 저장하세요. **당신(AI)이 이 오퍼를 종합 평가해 ai_score(0~100)와 verdict(최종평 — "어떤 상황이면 이 회사가 좋다" 식 한두 문장)를 함께 채워 저장하세요** — CareerMate엔 LLM이 없어 이 점수·평이 대시보드에 그대로 표시됩니다. 회사 평판이 판단에 필요하면 공고의 reputation을 먼저 채워두세요. 여러 오퍼의 정밀 비교는 compare_offers로 데이터를 받아 당신이 직접 하세요(총보상·성장·안정성·워라밸). **오퍼의 진행 상태는 offer_status로 기록하세요** — open(기본, 진행 중)/accepted(수락)/declined(거절). 사용자가 "그 오퍼 거절했어/수락하기로 했어"라고 하면 이 도구로 offer_status만 갱신하면 됩니다 — 삭제하지 않습니다(연봉·조건 기록은 다음 협상의 자산이고, 대시보드가 지난 오퍼로 접어서 보여줍니다). accepted로 저장하면 지원 상태(최종 합격) 갱신도 함께 제안하세요.',
|
|
1134
|
+
inputSchema: OfferInputSchema.shape,
|
|
1135
|
+
handler: (args) => {
|
|
1136
|
+
try {
|
|
1137
|
+
const offer = saveOffer(args);
|
|
1138
|
+
const total = offer.total_comp_annual_est;
|
|
1139
|
+
const statusLabel =
|
|
1140
|
+
offer.offer_status === 'accepted' ? ' · 수락' : offer.offer_status === 'declined' ? ' · 거절' : '';
|
|
1141
|
+
const user_message =
|
|
1142
|
+
`'${offer.company}' 오퍼를 저장했어요` +
|
|
1143
|
+
(offer.ai_score != null ? ` (점수 ${offer.ai_score})` : '') +
|
|
1144
|
+
(total != null ? ` · 추정 연 현금성 ${total.toLocaleString()}만원` : '') +
|
|
1145
|
+
statusLabel +
|
|
1146
|
+
'.';
|
|
1147
|
+
// 수락한 오퍼인데 지원 상태가 아직 최종 합격이 아니면 상태 갱신을 제안한다
|
|
1148
|
+
// (오퍼 상태 = 내 응답, 지원 상태 = 전형 결과 — 직교 축이라 자동 변경 대신 제안).
|
|
1149
|
+
const app = applicationRepo.getByJob(offer.job_id);
|
|
1150
|
+
const suggested_next_action =
|
|
1151
|
+
offer.offer_status === 'accepted' && app?.status !== 'final_passed'
|
|
1152
|
+
? {
|
|
1153
|
+
prompt: "지원 상태도 '최종 합격'으로 바꿀까요?",
|
|
1154
|
+
next_tool: 'update_application_status',
|
|
1155
|
+
}
|
|
1156
|
+
: null;
|
|
1157
|
+
const msg = suggested_next_action
|
|
1158
|
+
? `${user_message}\n\n사용자에게 지원 상태를 '최종 합격'으로 함께 바꿀지 물어보세요.`
|
|
1159
|
+
: user_message;
|
|
1160
|
+
return ok(msg, { ...offer, user_message, suggested_next_action });
|
|
1161
|
+
} catch (e) {
|
|
1162
|
+
return fail(e instanceof Error ? e.message : '저장 실패');
|
|
1163
|
+
}
|
|
1164
|
+
},
|
|
1165
|
+
},
|
|
1166
|
+
{
|
|
1167
|
+
name: 'compare_offers',
|
|
1168
|
+
title: '오퍼 비교',
|
|
1169
|
+
description:
|
|
1170
|
+
'저장된 모든 오퍼를 **점수순(당신이 매긴 ai_score)** 으로 회사·직무·추정 연 현금성 총액·근무형태·수락마감·최종평과 함께 돌려줍니다. total_comp_annual_est는 CareerMate가 결정론으로 합산한 추정치(기본급+성과급+복지, 일시금 제외)입니다. 각 오퍼에는 offer_status(open/accepted/declined)가 포함되니 **비교는 진행 중(open) 오퍼 위주로** 하고, 수락·거절된 지난 오퍼는 기준점(과거 조건)으로만 참고하세요. "어느 오퍼가 나은지"의 정밀 판단은 이 데이터를 받아 당신이 offer-evaluation·salary-negotiation 관점(총보상·성장·안정성·워라밸·회사 평판)으로 직접 하세요. 사용자가 "오퍼 비교해줘"라고 할 때 사용합니다. 읽기 전용입니다.',
|
|
1171
|
+
inputSchema: {},
|
|
1172
|
+
readOnly: true,
|
|
1173
|
+
handler: () => {
|
|
1174
|
+
const offers = compareOffers();
|
|
1175
|
+
if (offers.length === 0) return ok('저장된 오퍼가 없습니다. save_offer로 먼저 저장하세요.', offers);
|
|
1176
|
+
return ok(`오퍼 ${offers.length}건(점수순).`, offers);
|
|
1177
|
+
},
|
|
1178
|
+
},
|
|
1179
|
+
{
|
|
1180
|
+
name: 'get_offer',
|
|
1181
|
+
title: '오퍼 조회(공고별)',
|
|
1182
|
+
description: '특정 공고에 저장된 오퍼 1건을 돌려줍니다(없으면 null). 읽기 전용입니다.',
|
|
1183
|
+
inputSchema: { job_id: z.string() },
|
|
1184
|
+
readOnly: true,
|
|
1185
|
+
handler: (args) => {
|
|
1186
|
+
const offer = getOffer(args.job_id);
|
|
1187
|
+
if (!offer) return ok('이 공고에 저장된 오퍼가 없습니다.', null);
|
|
1188
|
+
return ok(`'${offer.company}' 오퍼.`, offer);
|
|
1072
1189
|
},
|
|
1073
1190
|
},
|
|
1074
1191
|
|
|
@@ -1159,7 +1276,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
1159
1276
|
name: 'validate_cover_letter',
|
|
1160
1277
|
title: '자기소개서 자동 점검 (저장 전 미리보기)',
|
|
1161
1278
|
description:
|
|
1162
|
-
'자기소개서를 save_cover_letter_version으로 저장하기 직전에 호출하면, 저장하지 않고 미리 점검만 합니다. CareerMate가 LLM 없이 결정론으로 (1) 본문의 정량 수치가 저장된 경력·이력서·프로젝트에 근거가 있는지(없으면 환각 의심 → 저장 시 차단됨), (2) 공고에서 가져온 수치를 본인 성과처럼 쓰지 않았는지, (3) 번역투·제너릭 도입·빈 형용사·블라인드 위반 같은 문체 신호를 세어 돌려줍니다. 또 (4) 글자수를 공백포함·공백제외·UTF-8 byte 세 기준으로 정확히 세고, min_chars/max_chars를 주면 상·하한 충족 여부까지 돌려줍니다(LLM은 글자수를 정확히 못 세므로 이 기능을 쓰세요). 점검은 셀 수 있는 것만 봅니다 — 사실성·동문서답 같은 의미 판단은 당신(AI)이 직접 하세요. 한국 문항형 자소서는 문항마다 글자수 한도가 다르니 각 문항 본문을 그 문항의 한도로 따로 점검하세요. job_id를 주면 공고 수치까지 비교합니다.',
|
|
1279
|
+
'자기소개서를 save_cover_letter_version으로 저장하기 직전에 호출하면, 저장하지 않고 미리 점검만 합니다. CareerMate가 LLM 없이 결정론으로 (1) 본문의 정량 수치가 저장된 경력·이력서·프로젝트에 근거가 있는지(없으면 환각 의심 → 저장 시 차단됨), (2) 공고에서 가져온 수치를 본인 성과처럼 쓰지 않았는지, (3) 번역투·제너릭 도입·빈 형용사·블라인드 위반 같은 문체 신호를 세어 돌려줍니다. 또 (4) 글자수를 공백포함·공백제외·UTF-8 byte 세 기준으로 정확히 세고, min_chars/max_chars를 주면 상·하한 충족 여부까지 돌려줍니다(LLM은 글자수를 정확히 못 세므로 이 기능을 쓰세요). 점검은 셀 수 있는 것만 봅니다 — 사실성·동문서답 같은 의미 판단은 당신(AI)이 직접 하세요. 한국 문항형 자소서는 문항마다 글자수 한도가 다르니 각 문항 본문을 그 문항의 한도로 따로 점검하세요. 공고에 questions가 저장돼 있으면 그 문항의 한도를 min_chars/max_chars/count_mode로 넘겨 점검하세요. job_id를 주면 공고 수치까지 비교합니다.',
|
|
1163
1280
|
inputSchema: {
|
|
1164
1281
|
text: z.string().max(200_000).describe('점검할 자기소개서 본문(문항형이면 한 문항 본문)'),
|
|
1165
1282
|
job_id: z.string().optional().describe('연결할 공고(있으면 공고 수치까지 비교)'),
|
|
@@ -1206,6 +1323,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
1206
1323
|
},
|
|
1207
1324
|
{
|
|
1208
1325
|
name: 'set_verify_mode',
|
|
1326
|
+
idempotent: true, // 같은 값 재설정은 무해
|
|
1209
1327
|
title: '점검 모드 설정 (기본/엄격)',
|
|
1210
1328
|
description:
|
|
1211
1329
|
'저장 전 자동 점검의 엄격도를 바꿉니다. 엄격 모드(strict=true)를 켜면, 이후 모든 자소서 저장에서 "이력서 본문에 근거 없는 수치"(구조화 경력 항목에만 있는 값)까지 차단합니다. 기본 모드는 출처가 아예 없는 수치만 차단합니다. 사용자가 "엄격하게 봐줘"라고 하면 바로 켜지 말고 "앞으로 항상 엄격하게 볼까요?"라고 먼저 확인한 뒤 동의하면 strict=true로 호출하세요. 끄기(strict=false)는 안전장치를 약하게 만드는 것이므로, 사용자가 명확히 끄겠다고 할 때만 호출하세요. 이번 한 건만 엄격하게 보려면 모드를 바꾸지 말고 validate_cover_letter / save_cover_letter_version에 strict:true를 한 번만 넘기세요.',
|
|
@@ -1254,6 +1372,8 @@ export const TOOLS: ToolDef[] = [
|
|
|
1254
1372
|
/* ----------------------------------------------------------------- update */
|
|
1255
1373
|
{
|
|
1256
1374
|
name: 'check_for_update',
|
|
1375
|
+
// npm 레지스트리로 버전 조회가 나간다(사용자 데이터 미전송).
|
|
1376
|
+
openWorld: true,
|
|
1257
1377
|
title: '업데이트 확인',
|
|
1258
1378
|
description:
|
|
1259
1379
|
'CareerMate에 새 버전이 있는지 npm 레지스트리에서 지금 확인합니다(공개 버전 정보만 GET하며 사용자 데이터는 전송하지 않습니다). 현재 버전·최신 버전·업데이트 가능 여부를 돌려줍니다. 사용자가 "최신이야?/업데이트 있어?"라고 묻거나, get_onboarding_status가 새 버전을 알릴 때 호출하세요. 확인만 하고 설치는 하지 않습니다. 사용자가 "업데이트해줘/최신으로 올려줘"처럼 명시적으로 요청하면 update_careermate를 호출하세요.',
|
|
@@ -1282,6 +1402,9 @@ export const TOOLS: ToolDef[] = [
|
|
|
1282
1402
|
},
|
|
1283
1403
|
{
|
|
1284
1404
|
name: 'update_careermate',
|
|
1405
|
+
// npm에서 패키지를 내려받아 설치 폴더의 코드를 교체한다.
|
|
1406
|
+
openWorld: true,
|
|
1407
|
+
destructive: true,
|
|
1285
1408
|
title: 'CareerMate 업데이트',
|
|
1286
1409
|
description:
|
|
1287
1410
|
'CareerMate를 최신 버전으로 업데이트합니다(설치 폴더에 npm으로 careermate@latest 설치). 사용자가 명시적으로 "업데이트해줘", "CareerMate 업데이트해줘", "최신으로 올려줘"라고 할 때 호출하세요. 이 도구가 최신 버전 확인도 함께 하므로 별도 확인 질문 없이 실행해도 됩니다. 성공하면 적용을 위해 AI 앱(또는 MCP 연결) 재시작이 필요하며, 재시작 시 데이터베이스는 자동 마이그레이션되어 기존 데이터는 보존됩니다. 자동 설치가 막히면(개발 모드·비표준 설치) 직접 칠 명령을 안내합니다.',
|
|
@@ -75,9 +75,15 @@ const MAX_BYTES = 25 * 1024 * 1024;
|
|
|
75
75
|
* "to finish the analysis". None of these are ever a résumé/career document, so
|
|
76
76
|
* declining them costs nothing and closes a real exfiltration amplifier.
|
|
77
77
|
* (`.key` is intentionally absent — it's Apple Keynote, handled as legacy-binary.)
|
|
78
|
+
*
|
|
79
|
+
* Beyond keys/dotenv, also decline (pre-launch audit follow-up):
|
|
80
|
+
* - cloud/cluster credential dirs: .kube, .docker, .azure, .boto, .config/gcloud
|
|
81
|
+
* - browser credential/cookie stores: Chrome·Edge "Login Data"/"Web Data"/Cookies,
|
|
82
|
+
* Firefox key4.db/logins.json
|
|
83
|
+
* - crypto wallets: wallet.dat
|
|
78
84
|
*/
|
|
79
85
|
const SENSITIVE_PATH_RE =
|
|
80
|
-
/(^|[/\\])\.(ssh|aws|gnupg)([/\\]|$)|(^|[/\\])id_(rsa|dsa|ecdsa|ed25519)(\.[\w]+)?$|\.(pem|ppk)$|(^|[/\\])\.env(\.[\w-]+)?$|(^|[/\\])\.(npmrc|netrc|git-credentials)$/i;
|
|
86
|
+
/(^|[/\\])\.(ssh|aws|gnupg|kube|docker|azure|boto)([/\\]|$)|[/\\]\.config[/\\]gcloud([/\\]|$)|(^|[/\\])id_(rsa|dsa|ecdsa|ed25519)(\.[\w]+)?$|\.(pem|ppk)$|(^|[/\\])\.env(\.[\w-]+)?$|(^|[/\\])\.(npmrc|netrc|git-credentials)$|(^|[/\\])(login data|web data|cookies|key4\.db|logins\.json|wallet\.dat)$/i;
|
|
81
87
|
|
|
82
88
|
// Bound the work a single (possibly hostile) file can trigger. The MAX_BYTES cap
|
|
83
89
|
// only limits the *compressed* input; a crafted archive can still expand far
|
|
@@ -38,12 +38,15 @@ export const HUMANIZE_WRITING_GUIDE = `# 사람이 쓴 것처럼: 자기소개
|
|
|
38
38
|
3. 기계적 병렬구조 — 세 개씩 나열(rule of three), 모든 문단이 똑같은 골격.
|
|
39
39
|
- "첫째…둘째…셋째", "A하고 B하며 C하는" 같은 균일한 나열을 반복하지 않는다.
|
|
40
40
|
- 문단마다 길이와 시작 방식을 일부러 다르게 한다.
|
|
41
|
+
- 같은 캐치프레이즈·모티프(예: "1원도 틀리지 않는")를 문서 전체에서 **최대 2회**까지만 쓴다. 3회부터는 인위적 장치로 읽혀 오히려 사람 냄새를 지운다 — 반복하고 싶으면 매번 다른 말로 바꿔 표현한다.
|
|
41
42
|
|
|
42
43
|
4. 상투적 연결어 남발 — "또한", "뿐만 아니라", "더 나아가", "이를 통해", "결론적으로".
|
|
43
44
|
- 접속어 없이 문장을 잇거나, 사건의 흐름 자체로 연결한다.
|
|
44
45
|
|
|
45
46
|
5. 균일한 문장 리듬 — 모든 문장이 비슷한 길이.
|
|
46
47
|
- 짧은 문장과 긴 문장을 섞는다. 가끔 아주 짧게 끊는다. 이렇게.
|
|
48
|
+
- 비슷한 길이의 문장을 세 개 연속으로 쓰지 않는다. 하나는 길게 풀고 다음은 툭 끊는 식으로 호흡을 바꾼다.
|
|
49
|
+
- 문단마다 한 호흡에 읽히는 짧은 단타 문장(8단어 안팎 이하)을 최소 하나 넣는다.
|
|
47
50
|
|
|
48
51
|
6. 과장된 수식·수동태 — "매우", "정말로", "탁월한", "성공적으로", "~되어졌습니다".
|
|
49
52
|
- 부사를 빼고 동사·명사를 구체화한다. 능동태를 기본으로 한다.
|
|
@@ -82,6 +85,7 @@ export const STRUCTURED_DOC_WRITING_GUIDE = `# 구조형 문서(경력기술서
|
|
|
82
85
|
- 각 불릿은 동사나 수치로 시작하고, 검증 가능한 사실(숫자·기간·도구·역할·결과)을 한 줄에 담는다.
|
|
83
86
|
- 불릿 안의 번역투·클리셰·공허한 일반론·수동태는 자기소개서와 똑같이 제거한다. (예: "분석을 진행하였습니다" → "분석했다", "많은 성과를 냈습니다" → 실제 수치)
|
|
84
87
|
- STAR을 한두 줄로 압축한다: 무엇을 왜 했고 결과 수치가 어떻게 됐는지.
|
|
88
|
+
- 산문형 불릿이 여러 문장으로 길어지면, 맨 앞에 "방법→성과"를 압축한 리드 구를 **굵게 한 줄** 얹어 30초 스캔에서 방법이 문장 속에 묻히지 않게 한다. (예: **"제품군별 소진주기 차등 → 90일 재구매 18%→27%"** — 이어서 상세 설명.) 단 정직 장치(귀속 단서·과장 없는 하향 정렬)는 리드 구에서도 그대로 유지한다 — 리드 구가 성과를 부풀리는 헤드라인이 되면 안 된다.
|
|
85
89
|
|
|
86
90
|
## Do / Don't
|
|
87
91
|
- Do: 두괄식 불릿(결과·수치를 앞세움), 직무·기간·역할 구분, 성과를 숫자로.
|
|
@@ -380,6 +380,49 @@ export type CoverLetterRecord = z.infer<typeof CoverLetterRecordSchema>;
|
|
|
380
380
|
|
|
381
381
|
/* --------------------------------------------------------------------- Job */
|
|
382
382
|
|
|
383
|
+
/**
|
|
384
|
+
* One company-specified cover-letter question (한국 문항형 자소서). Stored on the job so
|
|
385
|
+
* the AI writes each answer to its own prompt and checks length against that question's
|
|
386
|
+
* limit. CareerMate counts chars deterministically (checkCharLimit) — it never generates.
|
|
387
|
+
*/
|
|
388
|
+
export const JobQuestionSchema = z.object({
|
|
389
|
+
no: z.number().int().positive().optional().describe('문항 번호(있으면)'),
|
|
390
|
+
question: z.string().max(MAX_BODY).describe('문항 텍스트(회사가 제시한 자소서 항목)'),
|
|
391
|
+
min_chars: z.number().int().positive().optional().describe('글자수 하한(있으면)'),
|
|
392
|
+
max_chars: z.number().int().positive().optional().describe('글자수 상한(있으면)'),
|
|
393
|
+
count_mode: z
|
|
394
|
+
.enum(['with_space', 'no_space', 'byte'])
|
|
395
|
+
.optional()
|
|
396
|
+
.describe('글자수 기준: with_space=공백포함(기본)·no_space=공백제외·byte=UTF-8 바이트'),
|
|
397
|
+
note: optLine.describe('부가 메모(예: "필수", "경험 중심")'),
|
|
398
|
+
});
|
|
399
|
+
export type JobQuestion = z.infer<typeof JobQuestionSchema>;
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* One recruitment-schedule entry (전형 일정 — 면접·인적성·발표 등). Kept on the job so
|
|
403
|
+
* save_job_posting carries it without a new tool, and the server computes D-day/conflicts
|
|
404
|
+
* deterministically (getActionDigest). This is NOT the document deadline — that stays in
|
|
405
|
+
* `deadline`; these are the downstream stage dates the AI extracts from an interview invite
|
|
406
|
+
* or announcement the user pastes in. Dates are strict YYYY-MM-DD so the server can compute
|
|
407
|
+
* D-day and same-day collisions without guessing formats.
|
|
408
|
+
*/
|
|
409
|
+
const SCHEDULE_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
410
|
+
const SCHEDULE_TIME_RE = /^([01]\d|2[0-3]):[0-5]\d$/;
|
|
411
|
+
export const JobScheduleItemSchema = z.object({
|
|
412
|
+
label: reqLine.describe('일정 이름 (예: 1차 면접·AI역검·서류 발표)'),
|
|
413
|
+
date: z
|
|
414
|
+
.string()
|
|
415
|
+
.regex(SCHEDULE_DATE_RE, 'YYYY-MM-DD 형식이어야 합니다')
|
|
416
|
+
.describe('일정 날짜 YYYY-MM-DD'),
|
|
417
|
+
time: z
|
|
418
|
+
.string()
|
|
419
|
+
.regex(SCHEDULE_TIME_RE, 'HH:MM(24시간) 형식이어야 합니다')
|
|
420
|
+
.optional()
|
|
421
|
+
.describe('시각 HH:MM(24시간, 선택)'),
|
|
422
|
+
note: optLine.describe('부가 메모(예: 장소·준비물, 선택)'),
|
|
423
|
+
});
|
|
424
|
+
export type JobScheduleItem = z.infer<typeof JobScheduleItemSchema>;
|
|
425
|
+
|
|
383
426
|
export const JobInputSchema = z.object({
|
|
384
427
|
company: reqLine,
|
|
385
428
|
position: reqLine.describe('직무/포지션명'),
|
|
@@ -394,6 +437,17 @@ export const JobInputSchema = z.object({
|
|
|
394
437
|
company_overview: optBody.describe('회사 사업개요·미션·주요 제품·최근 활동 (AI가 리서치해 채움)'),
|
|
395
438
|
talent_profile: optBody.describe('회사가 강조하는 인재상 (한국 자소서·면접에서 정렬 기준)'),
|
|
396
439
|
core_values: strList.optional().describe('회사 핵심가치 (키워드 배열)'),
|
|
440
|
+
reputation: optBody.describe('회사 평판 요약 — 평점·워라밸·문화 리스크 등 (AI가 company-research 플레이북으로 리서치해 채움; 출처·최근성 표기)'),
|
|
441
|
+
questions: z
|
|
442
|
+
.array(JobQuestionSchema)
|
|
443
|
+
.max(MAX_ITEMS)
|
|
444
|
+
.optional()
|
|
445
|
+
.describe('회사 지정 자소서 문항·글자수 — 사용자가 문항을 주면 구조화 저장'),
|
|
446
|
+
schedule: z
|
|
447
|
+
.array(JobScheduleItemSchema)
|
|
448
|
+
.max(MAX_ITEMS)
|
|
449
|
+
.optional()
|
|
450
|
+
.describe('전형 일정 — 면접초대 메일·발표 공지를 받으면 구조화 저장(label·date 필수, time 선택). 서류 마감은 deadline 필드를 쓰세요.'),
|
|
397
451
|
});
|
|
398
452
|
export type JobInput = z.infer<typeof JobInputSchema>;
|
|
399
453
|
|
|
@@ -412,9 +466,65 @@ export const JobRecordSchema = z.object({
|
|
|
412
466
|
company_overview: z.string().nullable(),
|
|
413
467
|
talent_profile: z.string().nullable(),
|
|
414
468
|
core_values: z.array(z.string()),
|
|
469
|
+
reputation: z.string().nullable(),
|
|
470
|
+
questions: z.array(JobQuestionSchema),
|
|
471
|
+
schedule: z.array(JobScheduleItemSchema),
|
|
415
472
|
});
|
|
416
473
|
export type JobRecord = z.infer<typeof JobRecordSchema>;
|
|
417
474
|
|
|
475
|
+
/* ------------------------------------------------------------------- Offer */
|
|
476
|
+
|
|
477
|
+
/** Offer lifecycle — MY response to the company's proposal. Orthogonal to the
|
|
478
|
+
* application's 8-stage status (which tracks the COMPANY's process): together
|
|
479
|
+
* they can express e.g. "최종 합격했지만 내가 거절" (final_passed + declined). */
|
|
480
|
+
export const OFFER_STATUSES = ['open', 'accepted', 'declined'] as const;
|
|
481
|
+
export type OfferStatus = (typeof OFFER_STATUSES)[number];
|
|
482
|
+
|
|
483
|
+
/** Job offer for comparison. Cash pieces are 만원(numeric) so the server can sum an
|
|
484
|
+
* annual estimate deterministically; equity/other stay free-text. ai_score/verdict
|
|
485
|
+
* are produced by the connected AI at save time (CareerMate has no LLM). */
|
|
486
|
+
export const OfferInputSchema = z.object({
|
|
487
|
+
job_id: reqLine,
|
|
488
|
+
offer_status: z
|
|
489
|
+
.enum(OFFER_STATUSES)
|
|
490
|
+
.optional()
|
|
491
|
+
.describe('오퍼 상태 — open(진행 중)/accepted(수락)/declined(거절). 생략 시 기존 값 유지(신규 저장은 open)'),
|
|
492
|
+
base_salary: z.number().nonnegative().optional().describe('기본급/연봉 (만원)'),
|
|
493
|
+
bonus_amount: z.number().nonnegative().optional().describe('연 성과급·상여 기대액 (만원)'),
|
|
494
|
+
welfare_amount: z.number().nonnegative().optional().describe('복지포인트 등 연환산 (만원)'),
|
|
495
|
+
signing_amount: z.number().nonnegative().optional().describe('사이닝보너스 일시금 (만원)'),
|
|
496
|
+
equity_note: optNote.describe('주식/스톡옵션 (정성 서술 — 가치평가가 모호하므로 텍스트)'),
|
|
497
|
+
comp_note: optBody.describe('기타 보상 자유 서술'),
|
|
498
|
+
work_arrangement: optLine.describe('근무형태 (재택/하이브리드/출근)'),
|
|
499
|
+
contract_type: optLine.describe('계약형태 (정규/계약 등)'),
|
|
500
|
+
accept_deadline: optLine.describe('수락 마감일 YYYY-MM-DD'),
|
|
501
|
+
ai_score: z.number().min(0).max(100).optional().describe('AI가 매긴 종합 점수 0~100 (저장 시 AI가 채움)'),
|
|
502
|
+
verdict: optBody.describe('최종평 — 어떤 상황이면 어디가 좋은지 등 (AI가 저장 시 작성)'),
|
|
503
|
+
notes: optNote,
|
|
504
|
+
});
|
|
505
|
+
export type OfferInput = z.infer<typeof OfferInputSchema>;
|
|
506
|
+
|
|
507
|
+
export const OfferRecordSchema = z.object({
|
|
508
|
+
...baseRecord,
|
|
509
|
+
job_id: z.string(),
|
|
510
|
+
offer_status: z.enum(OFFER_STATUSES),
|
|
511
|
+
base_salary: z.number().nullable(),
|
|
512
|
+
bonus_amount: z.number().nullable(),
|
|
513
|
+
welfare_amount: z.number().nullable(),
|
|
514
|
+
signing_amount: z.number().nullable(),
|
|
515
|
+
equity_note: z.string().nullable(),
|
|
516
|
+
comp_note: z.string().nullable(),
|
|
517
|
+
work_arrangement: z.string().nullable(),
|
|
518
|
+
contract_type: z.string().nullable(),
|
|
519
|
+
accept_deadline: z.string().nullable(),
|
|
520
|
+
ai_score: z.number().nullable(),
|
|
521
|
+
verdict: z.string().nullable(),
|
|
522
|
+
notes: z.string().nullable(),
|
|
523
|
+
/** Server-computed annual cash estimate = base + bonus + welfare (signing is one-time, excluded). Null if no numeric pieces. */
|
|
524
|
+
total_comp_annual_est: z.number().nullable(),
|
|
525
|
+
});
|
|
526
|
+
export type OfferRecord = z.infer<typeof OfferRecordSchema>;
|
|
527
|
+
|
|
418
528
|
/* ------------------------------------------------------------- FitAnalysis */
|
|
419
529
|
|
|
420
530
|
export const FitAnalysisInputSchema = z.object({
|
|
@@ -444,6 +554,23 @@ export type FitAnalysisRecord = z.infer<typeof FitAnalysisRecordSchema>;
|
|
|
444
554
|
|
|
445
555
|
/* ------------------------------------------------------------- Application */
|
|
446
556
|
|
|
557
|
+
/**
|
|
558
|
+
* 탈락 회고 — 어느 전형 단계에서 떨어졌는지. 탈락 사유를 알려주는 기업은 6%뿐이고 지원자
|
|
559
|
+
* 83%가 알고 싶어하므로, 이 자가 기록이 "서류는 붙는데 면접에서 떨어진다" 같은 패턴을 로컬
|
|
560
|
+
* 데이터로 드러내는 유일한 신호다. status='rejected'일 때만 의미가 있다.
|
|
561
|
+
*/
|
|
562
|
+
export const REJECTED_STAGES = ['document', 'assessment', 'interview', 'final', 'other'] as const;
|
|
563
|
+
export type RejectedStage = (typeof REJECTED_STAGES)[number];
|
|
564
|
+
|
|
565
|
+
/** Korean labels for the rejection stage (dashboard localizes separately via i18n). */
|
|
566
|
+
export const REJECTED_STAGE_LABELS: Record<RejectedStage, string> = {
|
|
567
|
+
document: '서류',
|
|
568
|
+
assessment: '인적성·AI역검',
|
|
569
|
+
interview: '면접',
|
|
570
|
+
final: '최종·처우',
|
|
571
|
+
other: '기타',
|
|
572
|
+
};
|
|
573
|
+
|
|
447
574
|
export const ApplicationInputSchema = z.object({
|
|
448
575
|
job_id: reqLine,
|
|
449
576
|
status: z.enum(APPLICATION_STATUSES).optional(),
|
|
@@ -469,6 +596,12 @@ export const ApplicationStatusUpdateSchema = z.object({
|
|
|
469
596
|
status: z.enum(APPLICATION_STATUSES),
|
|
470
597
|
note: optNote.describe('상태 변경 사유/메모'),
|
|
471
598
|
submission: ApplicationSubmissionSchema.optional().describe('지원 완료 기록에 남길 제출 자료 스냅샷'),
|
|
599
|
+
rejected_stage: z
|
|
600
|
+
.enum(REJECTED_STAGES)
|
|
601
|
+
.optional()
|
|
602
|
+
.describe(
|
|
603
|
+
"탈락 회고 — 'rejected'로 바꿀 때 어느 단계에서 떨어졌는지: 서류=document, 인적성·AI역검=assessment, 면접=interview, 최종·처우=final, 기타=other. rejected가 아닌 상태에는 저장되지 않습니다.",
|
|
604
|
+
),
|
|
472
605
|
});
|
|
473
606
|
export type ApplicationStatusUpdate = z.infer<typeof ApplicationStatusUpdateSchema>;
|
|
474
607
|
|
|
@@ -480,6 +613,7 @@ export const ApplicationRecordSchema = z.object({
|
|
|
480
613
|
cover_letter_id: z.string().nullable(),
|
|
481
614
|
applied_at: z.string().nullable(),
|
|
482
615
|
notes: z.string().nullable(),
|
|
616
|
+
rejected_stage: z.enum(REJECTED_STAGES).nullable(),
|
|
483
617
|
});
|
|
484
618
|
export type ApplicationRecord = z.infer<typeof ApplicationRecordSchema>;
|
|
485
619
|
|
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
|
+
// 오퍼(제안) — 로컬 저장/조회(save_fit_analysis 등과 동급), 외부 전송 없음.
|
|
403
|
+
'save_offer', 'compare_offers', 'get_offer',
|
|
402
404
|
// careermate 자신의 인박스 폴더·대시보드(127.0.0.1)만 여는 로컬 동작 + 인박스 내부 문서 읽기 → 사전허용 안전.
|
|
403
405
|
'read_inbox', 'open_inbox', 'open_dashboard', 'open_application',
|
|
404
406
|
];
|