careermate 0.0.7 → 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 +108 -91
- package/THIRD_PARTY_NOTICES.md +2 -1
- package/bin/careermate.mjs +11 -2
- package/dist/career-os/eop/cover-letter.md +13 -6
- package/dist/career-os/eop/interview-prep.md +1 -0
- package/dist/career-os/knowledge/cover-letter.md +7 -3
- package/dist/career-os/knowledge/fit-matching.md +2 -0
- package/dist/career-os/knowledge/verifiers/ai-detection-risk.md +57 -0
- package/dist/career-os/knowledge/verifiers/human-voice.md +7 -2
- package/dist/career-os/knowledge/verifiers/truthfulness.md +1 -0
- package/dist/doctor.mjs +19760 -1018
- package/dist/mcp.mjs +19783 -1035
- package/dist/migrate.mjs +289 -20
- package/dist/public/i18n/en/applications.js +19 -0
- package/dist/public/i18n/en/common.js +10 -0
- package/dist/public/i18n/en/documents.js +0 -10
- package/dist/public/i18n/en/home.js +5 -0
- package/dist/public/i18n/en/jobs.js +18 -1
- package/dist/public/i18n/en/profile.js +0 -5
- 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 +9 -0
- package/dist/public/i18n/ko/documents.js +0 -10
- package/dist/public/i18n/ko/home.js +5 -0
- package/dist/public/i18n/ko/jobs.js +18 -1
- package/dist/public/i18n/ko/profile.js +0 -5
- package/dist/public/i18n/ko/settings.js +5 -2
- package/dist/public/lib.js +101 -0
- package/dist/public/pages/applications.js +95 -15
- package/dist/public/pages/documents.js +20 -14
- package/dist/public/pages/home.js +38 -6
- package/dist/public/pages/interview.js +11 -4
- package/dist/public/pages/jobs.js +71 -1
- package/dist/public/pages/profile.js +2 -3
- package/dist/public/pages/settings.js +2 -2
- package/dist/public/styles.css +52 -4
- package/dist/web.mjs +19001 -758
- package/docs/FAQ.md +13 -3
- package/docs/MCP_TOOLS.md +32 -12
- package/docs/career-os/eop/cover-letter.md +13 -6
- package/docs/career-os/eop/interview-prep.md +1 -0
- package/docs/career-os/knowledge/cover-letter.md +7 -3
- package/docs/career-os/knowledge/fit-matching.md +2 -0
- package/docs/career-os/knowledge/verifiers/ai-detection-risk.md +57 -0
- package/docs/career-os/knowledge/verifiers/human-voice.md +7 -2
- package/docs/career-os/knowledge/verifiers/truthfulness.md +1 -0
- package/package.json +5 -2
- package/packages/core/src/index.ts +2 -0
- package/packages/core/src/jobsearch.ts +190 -0
- package/packages/core/src/offers.ts +41 -0
- package/packages/core/src/services.ts +29 -3
- package/packages/core/src/summary.ts +431 -9
- package/packages/core/src/verify/charcount.ts +87 -0
- package/packages/core/src/verify/index.ts +1 -0
- package/packages/core/src/verify/lint.ts +5 -0
- package/packages/db/src/connection.ts +70 -2
- package/packages/db/src/index.ts +1 -0
- package/packages/db/src/repositories.ts +179 -13
- package/packages/db/src/schema.ts +169 -8
- package/packages/exporters/src/docx.ts +165 -0
- package/packages/exporters/src/index.ts +142 -9
- package/packages/exporters/src/markdown.ts +16 -11
- package/packages/knowledge/src/index.ts +8 -3
- package/packages/mcp-tools/src/result.ts +9 -0
- package/packages/mcp-tools/src/tools.ts +303 -60
- package/packages/parsers/src/documents.ts +7 -1
- package/packages/prompts/src/humanize.ts +4 -0
- package/packages/shared/src/schemas.ts +198 -0
- package/scripts/init.ts +7 -0
|
@@ -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: 두괄식 불릿(결과·수치를 앞세움), 직무·기간·역할 구분, 성과를 숫자로.
|
|
@@ -114,6 +114,43 @@ export const LinkSchema = z.object({
|
|
|
114
114
|
});
|
|
115
115
|
export type Link = z.infer<typeof LinkSchema>;
|
|
116
116
|
|
|
117
|
+
/* Structured quantitative credentials (한국 이력서 스펙). Values are kept verbatim as
|
|
118
|
+
strings so the AI cites them exactly (학점 3.8, TOEIC 920) without re-deriving. */
|
|
119
|
+
export const EducationSchema = z.object({
|
|
120
|
+
school: reqLine.describe('학교명'),
|
|
121
|
+
degree: z.string().max(MAX_LINE).optional().describe('학위/과정 (고졸·학사·석사·박사 등)'),
|
|
122
|
+
major: z.string().max(MAX_LINE).optional().describe('전공'),
|
|
123
|
+
gpa: z.string().max(MAX_LINE).optional().describe('학점 (예: 3.8)'),
|
|
124
|
+
gpa_scale: z.string().max(MAX_LINE).optional().describe('학점 만점 (예: 4.5)'),
|
|
125
|
+
start_date: z.string().max(MAX_LINE).optional().describe('YYYY-MM'),
|
|
126
|
+
end_date: z.string().max(MAX_LINE).optional().describe('YYYY-MM (졸업/예정)'),
|
|
127
|
+
status: z.string().max(MAX_LINE).optional().describe('졸업/재학/휴학/중퇴/수료'),
|
|
128
|
+
});
|
|
129
|
+
export type Education = z.infer<typeof EducationSchema>;
|
|
130
|
+
|
|
131
|
+
export const CertificationSchema = z.object({
|
|
132
|
+
name: reqLine.describe('자격증/면허명'),
|
|
133
|
+
issuer: z.string().max(MAX_LINE).optional().describe('발급기관'),
|
|
134
|
+
date: z.string().max(MAX_LINE).optional().describe('취득일 YYYY-MM'),
|
|
135
|
+
score: z.string().max(MAX_LINE).optional().describe('점수/등급(있으면)'),
|
|
136
|
+
});
|
|
137
|
+
export type Certification = z.infer<typeof CertificationSchema>;
|
|
138
|
+
|
|
139
|
+
export const LanguageScoreSchema = z.object({
|
|
140
|
+
test: reqLine.describe('시험명 (TOEIC·OPIc·TOEIC Speaking·JLPT·HSK 등)'),
|
|
141
|
+
score: reqLine.describe('점수/등급 (예: 920, IH, N1)'),
|
|
142
|
+
date: z.string().max(MAX_LINE).optional().describe('응시/취득일 YYYY-MM'),
|
|
143
|
+
});
|
|
144
|
+
export type LanguageScore = z.infer<typeof LanguageScoreSchema>;
|
|
145
|
+
|
|
146
|
+
export const AwardSchema = z.object({
|
|
147
|
+
title: reqLine.describe('수상/대외활동명'),
|
|
148
|
+
issuer: z.string().max(MAX_LINE).optional().describe('주최/수여기관'),
|
|
149
|
+
date: z.string().max(MAX_LINE).optional().describe('YYYY-MM'),
|
|
150
|
+
description: z.string().max(MAX_NOTE).optional().describe('간단 설명'),
|
|
151
|
+
});
|
|
152
|
+
export type Award = z.infer<typeof AwardSchema>;
|
|
153
|
+
|
|
117
154
|
export const ProfileInputSchema = z.object({
|
|
118
155
|
name: reqLine.optional(),
|
|
119
156
|
email: optLine,
|
|
@@ -126,6 +163,10 @@ export const ProfileInputSchema = z.object({
|
|
|
126
163
|
preferred_tone: z.string().max(MAX_LINE).optional().describe('자기소개서 선호 문체 (예: 담백하고 구체적)'),
|
|
127
164
|
emphasis_points: strList.optional().describe('강조하고 싶은 핵심 포인트'),
|
|
128
165
|
links: z.array(LinkSchema).max(MAX_ITEMS).optional().describe('포트폴리오/깃허브/링크드인 등'),
|
|
166
|
+
education: z.array(EducationSchema).max(MAX_ITEMS).optional().describe('학력 (학교·학위·전공·학점)'),
|
|
167
|
+
certifications: z.array(CertificationSchema).max(MAX_ITEMS).optional().describe('자격증/면허'),
|
|
168
|
+
language_scores: z.array(LanguageScoreSchema).max(MAX_ITEMS).optional().describe('어학 점수 (토익·오픽 등)'),
|
|
169
|
+
awards: z.array(AwardSchema).max(MAX_ITEMS).optional().describe('수상/대외활동'),
|
|
129
170
|
});
|
|
130
171
|
export type ProfileInput = z.infer<typeof ProfileInputSchema>;
|
|
131
172
|
|
|
@@ -142,6 +183,10 @@ export const ProfileRecordSchema = z.object({
|
|
|
142
183
|
preferred_tone: z.string().nullable(),
|
|
143
184
|
emphasis_points: z.array(z.string()),
|
|
144
185
|
links: z.array(LinkSchema),
|
|
186
|
+
education: z.array(EducationSchema),
|
|
187
|
+
certifications: z.array(CertificationSchema),
|
|
188
|
+
language_scores: z.array(LanguageScoreSchema),
|
|
189
|
+
awards: z.array(AwardSchema),
|
|
145
190
|
});
|
|
146
191
|
export type ProfileRecord = z.infer<typeof ProfileRecordSchema>;
|
|
147
192
|
|
|
@@ -335,6 +380,49 @@ export type CoverLetterRecord = z.infer<typeof CoverLetterRecordSchema>;
|
|
|
335
380
|
|
|
336
381
|
/* --------------------------------------------------------------------- Job */
|
|
337
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
|
+
|
|
338
426
|
export const JobInputSchema = z.object({
|
|
339
427
|
company: reqLine,
|
|
340
428
|
position: reqLine.describe('직무/포지션명'),
|
|
@@ -346,6 +434,20 @@ export const JobInputSchema = z.object({
|
|
|
346
434
|
keywords: strList.optional().describe('핵심 키워드'),
|
|
347
435
|
deadline: z.string().max(MAX_LINE).optional().describe('마감일 YYYY-MM-DD'),
|
|
348
436
|
source: z.string().max(MAX_LINE).optional().describe('출처 (사람인/원티드/직접 입력 등)'),
|
|
437
|
+
company_overview: optBody.describe('회사 사업개요·미션·주요 제품·최근 활동 (AI가 리서치해 채움)'),
|
|
438
|
+
talent_profile: optBody.describe('회사가 강조하는 인재상 (한국 자소서·면접에서 정렬 기준)'),
|
|
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 필드를 쓰세요.'),
|
|
349
451
|
});
|
|
350
452
|
export type JobInput = z.infer<typeof JobInputSchema>;
|
|
351
453
|
|
|
@@ -361,9 +463,68 @@ export const JobRecordSchema = z.object({
|
|
|
361
463
|
keywords: z.array(z.string()),
|
|
362
464
|
deadline: z.string().nullable(),
|
|
363
465
|
source: z.string().nullable(),
|
|
466
|
+
company_overview: z.string().nullable(),
|
|
467
|
+
talent_profile: z.string().nullable(),
|
|
468
|
+
core_values: z.array(z.string()),
|
|
469
|
+
reputation: z.string().nullable(),
|
|
470
|
+
questions: z.array(JobQuestionSchema),
|
|
471
|
+
schedule: z.array(JobScheduleItemSchema),
|
|
364
472
|
});
|
|
365
473
|
export type JobRecord = z.infer<typeof JobRecordSchema>;
|
|
366
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
|
+
|
|
367
528
|
/* ------------------------------------------------------------- FitAnalysis */
|
|
368
529
|
|
|
369
530
|
export const FitAnalysisInputSchema = z.object({
|
|
@@ -393,6 +554,23 @@ export type FitAnalysisRecord = z.infer<typeof FitAnalysisRecordSchema>;
|
|
|
393
554
|
|
|
394
555
|
/* ------------------------------------------------------------- Application */
|
|
395
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
|
+
|
|
396
574
|
export const ApplicationInputSchema = z.object({
|
|
397
575
|
job_id: reqLine,
|
|
398
576
|
status: z.enum(APPLICATION_STATUSES).optional(),
|
|
@@ -418,6 +596,12 @@ export const ApplicationStatusUpdateSchema = z.object({
|
|
|
418
596
|
status: z.enum(APPLICATION_STATUSES),
|
|
419
597
|
note: optNote.describe('상태 변경 사유/메모'),
|
|
420
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
|
+
),
|
|
421
605
|
});
|
|
422
606
|
export type ApplicationStatusUpdate = z.infer<typeof ApplicationStatusUpdateSchema>;
|
|
423
607
|
|
|
@@ -429,6 +613,7 @@ export const ApplicationRecordSchema = z.object({
|
|
|
429
613
|
cover_letter_id: z.string().nullable(),
|
|
430
614
|
applied_at: z.string().nullable(),
|
|
431
615
|
notes: z.string().nullable(),
|
|
616
|
+
rejected_stage: z.enum(REJECTED_STAGES).nullable(),
|
|
432
617
|
});
|
|
433
618
|
export type ApplicationRecord = z.infer<typeof ApplicationRecordSchema>;
|
|
434
619
|
|
|
@@ -459,12 +644,24 @@ export const InterviewQuestionSchema = z.object({
|
|
|
459
644
|
});
|
|
460
645
|
export type InterviewQuestion = z.infer<typeof InterviewQuestionSchema>;
|
|
461
646
|
|
|
647
|
+
/** Post-interview debrief — captured AFTER the interview to close the loop and feed the next round. */
|
|
648
|
+
export const InterviewDebriefSchema = z.object({
|
|
649
|
+
asked_questions: strList.optional().describe('실제로 받은 질문들'),
|
|
650
|
+
went_well: optBody.describe('잘된 점/강했던 답변'),
|
|
651
|
+
to_improve: optBody.describe('아쉬운 점/다음에 개선할 점'),
|
|
652
|
+
next_round_focus: optBody.describe('다음 라운드/면접에 대비해 준비할 포인트'),
|
|
653
|
+
sentiment: z.enum(['positive', 'neutral', 'negative']).optional().describe('체감 분위기'),
|
|
654
|
+
interviewed_at: optLine.describe('면접 본 날짜(YYYY-MM-DD)'),
|
|
655
|
+
});
|
|
656
|
+
export type InterviewDebrief = z.infer<typeof InterviewDebriefSchema>;
|
|
657
|
+
|
|
462
658
|
export const InterviewPrepInputSchema = z.object({
|
|
463
659
|
job_id: reqLine,
|
|
464
660
|
questions: z.array(InterviewQuestionSchema).max(MAX_ITEMS).optional(),
|
|
465
661
|
star_guides: z.array(StarGuideSchema).max(MAX_ITEMS).optional(),
|
|
466
662
|
self_introduction: optBody.describe('1분 자기소개 초안'),
|
|
467
663
|
notes: optNote.describe('면접 후기/메모'),
|
|
664
|
+
debrief: InterviewDebriefSchema.optional().describe('면접 후 디브리프(받은 질문·잘된 점·개선점·다음 라운드 포커스)'),
|
|
468
665
|
});
|
|
469
666
|
export type InterviewPrepInput = z.infer<typeof InterviewPrepInputSchema>;
|
|
470
667
|
|
|
@@ -475,6 +672,7 @@ export const InterviewPrepRecordSchema = z.object({
|
|
|
475
672
|
star_guides: z.array(StarGuideSchema),
|
|
476
673
|
self_introduction: z.string().nullable(),
|
|
477
674
|
notes: z.string().nullable(),
|
|
675
|
+
debrief: InterviewDebriefSchema.nullable(),
|
|
478
676
|
});
|
|
479
677
|
export type InterviewPrepRecord = z.infer<typeof InterviewPrepRecordSchema>;
|
|
480
678
|
|
package/scripts/init.ts
CHANGED
|
@@ -394,6 +394,13 @@ const SAFE_TOOLS: readonly string[] = [
|
|
|
394
394
|
'get_application_context', 'save_fit_analysis', 'update_application_status', 'save_interview_prep',
|
|
395
395
|
'export_cover_letter', 'export_resume', 'export_profile', 'list_recent_activity', 'get_workflow_guide', 'get_playbook', 'get_verifier',
|
|
396
396
|
'validate_cover_letter', 'set_verify_mode', 'get_writing_style_guide', 'check_for_update',
|
|
397
|
+
// 읽기전용 조회 도구(DB 변경·외부 전송 없음) → 사전허용 안전.
|
|
398
|
+
'get_followups', 'get_pipeline_stats', 'prescreen_job',
|
|
399
|
+
// search_jobs: 공개 잡보드에서 공고를 가져오기만(보내는 건 검색어뿐, 사용자 데이터 미전송).
|
|
400
|
+
// check_for_update와 같은 "네트워크 read" 부류라 사전허용 안전(우회·쓰기 없음).
|
|
401
|
+
'search_jobs',
|
|
402
|
+
// 오퍼(제안) — 로컬 저장/조회(save_fit_analysis 등과 동급), 외부 전송 없음.
|
|
403
|
+
'save_offer', 'compare_offers', 'get_offer',
|
|
397
404
|
// careermate 자신의 인박스 폴더·대시보드(127.0.0.1)만 여는 로컬 동작 + 인박스 내부 문서 읽기 → 사전허용 안전.
|
|
398
405
|
'read_inbox', 'open_inbox', 'open_dashboard', 'open_application',
|
|
399
406
|
];
|