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.
Files changed (71) hide show
  1. package/AGENTS.md +2 -2
  2. package/LICENSE +1 -1
  3. package/README.md +108 -91
  4. package/THIRD_PARTY_NOTICES.md +2 -1
  5. package/bin/careermate.mjs +11 -2
  6. package/dist/career-os/eop/cover-letter.md +13 -6
  7. package/dist/career-os/eop/interview-prep.md +1 -0
  8. package/dist/career-os/knowledge/cover-letter.md +7 -3
  9. package/dist/career-os/knowledge/fit-matching.md +2 -0
  10. package/dist/career-os/knowledge/verifiers/ai-detection-risk.md +57 -0
  11. package/dist/career-os/knowledge/verifiers/human-voice.md +7 -2
  12. package/dist/career-os/knowledge/verifiers/truthfulness.md +1 -0
  13. package/dist/doctor.mjs +19760 -1018
  14. package/dist/mcp.mjs +19783 -1035
  15. package/dist/migrate.mjs +289 -20
  16. package/dist/public/i18n/en/applications.js +19 -0
  17. package/dist/public/i18n/en/common.js +10 -0
  18. package/dist/public/i18n/en/documents.js +0 -10
  19. package/dist/public/i18n/en/home.js +5 -0
  20. package/dist/public/i18n/en/jobs.js +18 -1
  21. package/dist/public/i18n/en/profile.js +0 -5
  22. package/dist/public/i18n/en/settings.js +5 -2
  23. package/dist/public/i18n/ko/applications.js +19 -0
  24. package/dist/public/i18n/ko/common.js +9 -0
  25. package/dist/public/i18n/ko/documents.js +0 -10
  26. package/dist/public/i18n/ko/home.js +5 -0
  27. package/dist/public/i18n/ko/jobs.js +18 -1
  28. package/dist/public/i18n/ko/profile.js +0 -5
  29. package/dist/public/i18n/ko/settings.js +5 -2
  30. package/dist/public/lib.js +101 -0
  31. package/dist/public/pages/applications.js +95 -15
  32. package/dist/public/pages/documents.js +20 -14
  33. package/dist/public/pages/home.js +38 -6
  34. package/dist/public/pages/interview.js +11 -4
  35. package/dist/public/pages/jobs.js +71 -1
  36. package/dist/public/pages/profile.js +2 -3
  37. package/dist/public/pages/settings.js +2 -2
  38. package/dist/public/styles.css +52 -4
  39. package/dist/web.mjs +19001 -758
  40. package/docs/FAQ.md +13 -3
  41. package/docs/MCP_TOOLS.md +32 -12
  42. package/docs/career-os/eop/cover-letter.md +13 -6
  43. package/docs/career-os/eop/interview-prep.md +1 -0
  44. package/docs/career-os/knowledge/cover-letter.md +7 -3
  45. package/docs/career-os/knowledge/fit-matching.md +2 -0
  46. package/docs/career-os/knowledge/verifiers/ai-detection-risk.md +57 -0
  47. package/docs/career-os/knowledge/verifiers/human-voice.md +7 -2
  48. package/docs/career-os/knowledge/verifiers/truthfulness.md +1 -0
  49. package/package.json +5 -2
  50. package/packages/core/src/index.ts +2 -0
  51. package/packages/core/src/jobsearch.ts +190 -0
  52. package/packages/core/src/offers.ts +41 -0
  53. package/packages/core/src/services.ts +29 -3
  54. package/packages/core/src/summary.ts +431 -9
  55. package/packages/core/src/verify/charcount.ts +87 -0
  56. package/packages/core/src/verify/index.ts +1 -0
  57. package/packages/core/src/verify/lint.ts +5 -0
  58. package/packages/db/src/connection.ts +70 -2
  59. package/packages/db/src/index.ts +1 -0
  60. package/packages/db/src/repositories.ts +179 -13
  61. package/packages/db/src/schema.ts +169 -8
  62. package/packages/exporters/src/docx.ts +165 -0
  63. package/packages/exporters/src/index.ts +142 -9
  64. package/packages/exporters/src/markdown.ts +16 -11
  65. package/packages/knowledge/src/index.ts +8 -3
  66. package/packages/mcp-tools/src/result.ts +9 -0
  67. package/packages/mcp-tools/src/tools.ts +303 -60
  68. package/packages/parsers/src/documents.ts +7 -1
  69. package/packages/prompts/src/humanize.ts +4 -0
  70. package/packages/shared/src/schemas.ts +198 -0
  71. 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
  ];