careermate 0.0.4 → 0.0.5

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.
@@ -82,6 +82,8 @@ import { resolveDashboardUrl, openInBrowser, openInFileManager } from './bridge.
82
82
  import { getUpdateStatus, getUpdateStatusAsync, runSelfUpdate } from './update.ts';
83
83
 
84
84
  const STATUS_LIST = APPLICATION_STATUSES.map((s) => `${s}(${APPLICATION_STATUS_LABELS[s]})`).join(', ');
85
+ const DELETE_CONFIRMATION = 'DELETE';
86
+ const DASHBOARD_URL = 'http://127.0.0.1:4319';
85
87
 
86
88
  /** List real, user-dropped files in the inbox (skips hidden/OS/office-lock cruft). */
87
89
  function listInboxFiles(dir: string): { name: string; path: string }[] {
@@ -278,9 +280,19 @@ export const TOOLS: ToolDef[] = [
278
280
  `더 깐깐하게 보길 원하면 "엄격하게 봐줘"라고 하거나 대시보드 설정에서 엄격 모드를 켤 수 있어요(이력서 본문에 없는 수치까지 차단).`;
279
281
  }
280
282
  if (upd.update_available) {
281
- text += `\n\n🆕 새 버전 v${upd.latest} 사용 가능 (현재 v${upd.current}). 사용자가 원하면 update_careermate 도구로 업데이트하세요.`;
283
+ text += `\n\n새 버전 v${upd.latest}이 나왔습니다(현재 v${upd.current}). 사용자가 원하면 바로 업데이트를 진행하세요. 업데이트 후에는 AI 앱 또는 MCP 연결을 재시작해야 적용됩니다.`;
282
284
  }
283
- return ok(text, { ...s, verify_strict: strict, update: upd });
285
+ return ok(text, {
286
+ ...s,
287
+ verify_strict: strict,
288
+ update: upd,
289
+ suggested_next_action: upd.update_available
290
+ ? {
291
+ prompt: `새 버전 v${upd.latest}이 나왔습니다. 지금 업데이트할까요?`,
292
+ next_tool: 'update_careermate',
293
+ }
294
+ : null,
295
+ });
284
296
  },
285
297
  },
286
298
 
@@ -451,6 +463,32 @@ export const TOOLS: ToolDef[] = [
451
463
  }
452
464
  },
453
465
  },
466
+ {
467
+ name: 'delete_cover_letter',
468
+ title: '자기소개서 삭제',
469
+ description:
470
+ '사용자가 저장된 자기소개서 삭제를 명확히 요청했을 때만 사용합니다. 먼저 get_cover_letters로 전체 목록 또는 공고별 목록을 확인하고, 삭제할 항목을 사용자가 확인한 뒤 호출하세요. 삭제하면 해당 자기소개서의 모든 버전이 함께 삭제되며 되돌릴 수 없습니다. confirm에는 DELETE를 넣으세요.',
471
+ inputSchema: {
472
+ cover_letter_id: z.string(),
473
+ confirm: z.string().describe('삭제 확인값. 정확히 DELETE를 넣어야 삭제됩니다.'),
474
+ },
475
+ handler: (args) => {
476
+ if (args?.confirm !== DELETE_CONFIRMATION) {
477
+ return fail('삭제하려면 confirm에 DELETE를 넣어 다시 호출하세요.');
478
+ }
479
+ const cl = coverLetterRepo.get(args.cover_letter_id, false);
480
+ if (!cl) return fail('자기소개서를 찾을 수 없습니다.');
481
+ const removed = coverLetterRepo.remove(cl.id);
482
+ if (!removed) return fail('자기소개서를 삭제하지 못했습니다.');
483
+ const user_message = `'${cl.title}' 자기소개서를 삭제했어요. 연결된 지원 기록에서는 자기소개서 연결만 비워뒀습니다.`;
484
+ return ok(user_message, {
485
+ deleted: true,
486
+ cover_letter_id: cl.id,
487
+ title: cl.title,
488
+ user_message,
489
+ });
490
+ },
491
+ },
454
492
 
455
493
  /* ------------------------------------------------------------------ jobs */
456
494
  {
@@ -461,7 +499,15 @@ export const TOOLS: ToolDef[] = [
461
499
  inputSchema: JobInputSchema.shape,
462
500
  handler: (args) => {
463
501
  const { job } = saveJobPosting(args);
464
- return ok(`'${job.company} · ${job.position}' 공고를 저장했습니다. (job_id: ${job.id})`, job);
502
+ const user_message = `'${job.company} · ${job.position}' 공고를 저장했어요.`;
503
+ return ok(user_message, {
504
+ ...job,
505
+ user_message,
506
+ suggested_next_action: {
507
+ prompt: '이 공고와 내 프로필을 비교해 적합도를 분석할까요?',
508
+ next_workflow: 'analyze_job',
509
+ },
510
+ });
465
511
  },
466
512
  },
467
513
  {
@@ -472,7 +518,7 @@ export const TOOLS: ToolDef[] = [
472
518
  readOnly: true,
473
519
  handler: (args) => {
474
520
  const job = jobRepo.get(args.job_id);
475
- if (!job) return fail(`공고를 찾을 수 없습니다: ${args.job_id}`);
521
+ if (!job) return fail('공고를 찾을 수 없습니다.');
476
522
  return ok(`${job.company} · ${job.position}`, {
477
523
  job: jobWithMeta(job),
478
524
  fit: fitRepo.getByJob(job.id),
@@ -493,6 +539,38 @@ export const TOOLS: ToolDef[] = [
493
539
  return ok(`공고 ${jobs.length}건`, jobs);
494
540
  },
495
541
  },
542
+ {
543
+ name: 'delete_job_posting',
544
+ title: '채용공고 삭제',
545
+ description:
546
+ '사용자가 저장된 채용공고 삭제를 명확히 요청했을 때만 사용합니다. 먼저 list_jobs로 전체 공고 목록을 확인하고, 삭제할 공고를 사용자가 확인한 뒤 호출하세요. 삭제하면 연결된 지원 상태·적합도 분석·면접 준비가 함께 삭제됩니다. 연결된 자기소개서는 삭제하지 않고 공고 연결만 해제합니다. confirm에는 DELETE를 넣으세요.',
547
+ inputSchema: {
548
+ job_id: z.string(),
549
+ confirm: z.string().describe('삭제 확인값. 정확히 DELETE를 넣어야 삭제됩니다.'),
550
+ },
551
+ handler: (args) => {
552
+ if (args?.confirm !== DELETE_CONFIRMATION) {
553
+ return fail('삭제하려면 confirm에 DELETE를 넣어 다시 호출하세요.');
554
+ }
555
+ const job = jobRepo.get(args.job_id);
556
+ if (!job) return fail('공고를 찾을 수 없습니다.');
557
+ const detachedCoverLetters = coverLetterRepo.listByJob(job.id).length;
558
+ const removed = jobRepo.remove(job.id);
559
+ if (!removed) return fail('공고를 삭제하지 못했습니다.');
560
+ const detachedNote = detachedCoverLetters
561
+ ? ` 연결된 자기소개서 ${detachedCoverLetters}건은 삭제하지 않고 공고 연결만 해제했습니다.`
562
+ : '';
563
+ const user_message = `'${job.company} · ${job.position}' 공고를 삭제했어요. 연결된 지원 상태, 적합도 분석, 면접 준비도 함께 정리했습니다.${detachedNote}`;
564
+ return ok(user_message, {
565
+ deleted: true,
566
+ job_id: job.id,
567
+ company: job.company,
568
+ position: job.position,
569
+ detached_cover_letters: detachedCoverLetters,
570
+ user_message,
571
+ });
572
+ },
573
+ },
496
574
 
497
575
  /* ----------------------------------------------------- application context */
498
576
  {
@@ -520,8 +598,8 @@ export const TOOLS: ToolDef[] = [
520
598
  ctx.job ? `대상 공고: ${ctx.job.company} · ${ctx.job.position}` : '대상 공고 없음',
521
599
  ctx.writing_preferences.preferred_tone ? `선호 문체: ${ctx.writing_preferences.preferred_tone}` : '',
522
600
  recommended_route
523
- ? `💡 다음 단계: get_workflow_guide({ workflow_id: "${recommended_route}" })로 전문가 실행 절차(EOP)와 적용할 플레이북·검증 순서를 받아 적용하세요(즉답 금지).`
524
- : '💡 커리어 작업을 시작하려면 get_workflow_guide로 전문가 실행 절차와 검증 순서를 먼저 받으세요.',
601
+ ? `내부 안내: 사용자에게는 도구명이나 ID를 말하지 말고, 먼저 get_workflow_guide({ workflow_id: "${recommended_route}" })로 실행 절차와 검증 순서를 받은 뒤 쉬운 말로 안내하세요.`
602
+ : '내부 안내: 사용자에게는 도구명을 말하지 말고, 먼저 get_workflow_guide로 실행 절차와 검증 순서를 받은 뒤 쉬운 말로 안내하세요.',
525
603
  ].filter(Boolean).join('\n');
526
604
  return ok(summary, enriched);
527
605
  },
@@ -535,7 +613,34 @@ export const TOOLS: ToolDef[] = [
535
613
  handler: (args) => {
536
614
  try {
537
615
  const fit = saveFitAnalysis(args);
538
- return ok(`적합도 분석을 저장했습니다${fit.score != null ? ` (${fit.score}점)` : ''}.`, fit);
616
+ const suggested_next_action = {
617
+ prompt: '이 공고에 맞춘 자기소개서를 써드릴까요?',
618
+ options: [
619
+ {
620
+ label: '네, 이 공고에 맞춘 자기소개서를 써줘',
621
+ next_workflow: 'write_cover_letter',
622
+ },
623
+ {
624
+ label: '아니요, 분석만 저장해줘',
625
+ next_workflow: null,
626
+ },
627
+ ],
628
+ };
629
+ const user_message = `적합도 분석을 저장했어요${fit.score != null ? ` (${fit.score}점)` : ''}. 자세한 결과는 대시보드에서도 확인할 수 있습니다: ${DASHBOARD_URL}`;
630
+ return ok(
631
+ [
632
+ user_message,
633
+ '',
634
+ '사용자에게 다음 선택지를 보여주세요:',
635
+ '1. 네, 이 공고에 맞춘 자기소개서를 써줘',
636
+ '2. 아니요, 분석만 저장해줘',
637
+ ].join('\n'),
638
+ {
639
+ ...fit,
640
+ user_message,
641
+ suggested_next_action,
642
+ },
643
+ );
539
644
  } catch (e) {
540
645
  return fail(e instanceof Error ? e.message : '저장 실패');
541
646
  }
@@ -555,9 +660,21 @@ export const TOOLS: ToolDef[] = [
555
660
  handler: (args) => {
556
661
  try {
557
662
  const res = updateApplicationStatus(args.job_id, args.status, args.note);
663
+ const user_message =
664
+ `지원 상태를 '${APPLICATION_STATUS_LABELS[args.status as keyof typeof APPLICATION_STATUS_LABELS]}'(으)로 바꿨어요.` +
665
+ (res.hint ? `\n\n${res.hint}` : '');
558
666
  return ok(
559
- `상태를 '${APPLICATION_STATUS_LABELS[args.status as keyof typeof APPLICATION_STATUS_LABELS]}'(으)로 변경했습니다.${res.hint ? `\n\n💡 ${res.hint}` : ''}`,
560
- res,
667
+ user_message,
668
+ {
669
+ ...res,
670
+ user_message,
671
+ suggested_next_action: res.hint
672
+ ? {
673
+ prompt: res.hint,
674
+ next_workflow: 'prepare_interview',
675
+ }
676
+ : null,
677
+ },
561
678
  );
562
679
  } catch (e) {
563
680
  return fail(e instanceof Error ? e.message : '변경 실패');
@@ -575,7 +692,8 @@ export const TOOLS: ToolDef[] = [
575
692
  handler: (args) => {
576
693
  try {
577
694
  const prep = saveInterviewPrep(args);
578
- return ok(`면접 준비 자료를 저장했습니다 (질문 ${prep.questions.length}개).`, prep);
695
+ const user_message = `면접 준비 자료를 저장했어요. 예상 질문 ${prep.questions.length}개가 준비되어 있습니다.`;
696
+ return ok(user_message, { ...prep, user_message });
579
697
  } catch (e) {
580
698
  return fail(e instanceof Error ? e.message : '저장 실패');
581
699
  }
@@ -792,17 +910,27 @@ export const TOOLS: ToolDef[] = [
792
910
  name: 'check_for_update',
793
911
  title: '업데이트 확인',
794
912
  description:
795
- 'CareerMate에 새 버전이 있는지 npm 레지스트리에서 지금 확인합니다(공개 버전 정보만 GET하며 사용자 데이터는 전송하지 않습니다). 현재 버전·최신 버전·업데이트 가능 여부를 돌려줍니다. 사용자가 "최신이야?/업데이트 있어?"라고 묻거나, get_onboarding_status가 새 버전을 알릴 때 호출하세요. 실제 적용은 update_careermate로 합니다.',
913
+ 'CareerMate에 새 버전이 있는지 npm 레지스트리에서 지금 확인합니다(공개 버전 정보만 GET하며 사용자 데이터는 전송하지 않습니다). 현재 버전·최신 버전·업데이트 가능 여부를 돌려줍니다. 사용자가 "최신이야?/업데이트 있어?"라고 묻거나, get_onboarding_status가 새 버전을 알릴 때 호출하세요. 확인만 하고 설치는 하지 않습니다. 사용자가 "업데이트해줘/최신으로 올려줘"처럼 명시적으로 요청하면 update_careermate를 호출하세요.',
796
914
  inputSchema: {},
797
915
  readOnly: true,
798
916
  handler: async () => {
799
917
  const u = await getUpdateStatusAsync();
800
918
  if (u.latest === null) return ok('업데이트 확인에 실패했습니다(오프라인이거나 일시적 오류). 잠시 후 다시 시도하세요.', u);
919
+ const user_message = u.update_available
920
+ ? `새 버전 v${u.latest}이 나왔어요. 현재 버전은 v${u.current}입니다. 원하시면 지금 업데이트할 수 있습니다.`
921
+ : `최신 버전입니다 (v${u.current}).`;
801
922
  return ok(
802
- u.update_available
803
- ? `새 버전 v${u.latest} 사용 가능 (현재 v${u.current}). update_careermate로 업데이트할 수 있습니다.`
804
- : `최신 버전입니다 (v${u.current}).`,
805
- u,
923
+ user_message,
924
+ {
925
+ ...u,
926
+ user_message,
927
+ suggested_next_action: u.update_available
928
+ ? {
929
+ prompt: '지금 CareerMate를 업데이트할까요?',
930
+ next_tool: 'update_careermate',
931
+ }
932
+ : null,
933
+ },
806
934
  );
807
935
  },
808
936
  },
@@ -810,12 +938,12 @@ export const TOOLS: ToolDef[] = [
810
938
  name: 'update_careermate',
811
939
  title: 'CareerMate 업데이트',
812
940
  description:
813
- 'CareerMate를 최신 버전으로 업데이트합니다(설치 폴더에 npm으로 careermate@latest 설치). 사용자가 명시적으로 "업데이트해줘"라고 할 때만 호출하세요. 성공하면 적용을 위해 AI 앱(또는 MCP 연결) 재시작이 필요하며, 재시작 시 데이터베이스는 자동 마이그레이션되어 기존 데이터가 보존됩니다. 자동 설치가 막히면(개발 모드·비표준 설치) 직접 칠 명령을 안내합니다.',
941
+ 'CareerMate를 최신 버전으로 업데이트합니다(설치 폴더에 npm으로 careermate@latest 설치). 사용자가 명시적으로 "업데이트해줘", "CareerMate 업데이트해줘", "최신으로 올려줘"라고 할 때 호출하세요. 이 도구가 최신 버전 확인도 함께 하므로 별도 확인 질문 없이 실행해도 됩니다. 성공하면 적용을 위해 AI 앱(또는 MCP 연결) 재시작이 필요하며, 재시작 시 데이터베이스는 자동 마이그레이션되어 기존 데이터는 보존됩니다. 자동 설치가 막히면(개발 모드·비표준 설치) 직접 칠 명령을 안내합니다.',
814
942
  inputSchema: {},
815
943
  handler: async () => {
816
944
  const r = await runSelfUpdate();
817
945
  if (!r.ok) return fail(`${r.message}${r.manual ? `\n\n직접 실행: ${r.manual}` : ''}`);
818
- return ok(r.message, r);
946
+ return ok(r.message, { ...r, user_message: r.message });
819
947
  },
820
948
  },
821
949
  ];
@@ -325,6 +325,6 @@ export async function runSelfUpdate(): Promise<UpdateResult> {
325
325
  ok: true,
326
326
  from,
327
327
  to: latest,
328
- message: `careermate를 ${latest ? `v${latest}` : '최신 버전'}(으)로 업데이트했습니다. 적용하려면 AI 앱(또는 MCP 연결)을 재시작하세요. 재시작 시 데이터베이스는 자동으로 마이그레이션되며 기존 데이터는 보존됩니다.`,
328
+ message: `CareerMate를 ${latest ? `v${latest}` : '최신 버전'}(으)로 업데이트했습니다. 적용하려면 AI 앱(또는 MCP 연결)을 재시작하세요. 재시작 시 데이터베이스는 자동으로 마이그레이션되며 기존 데이터는 보존됩니다.`,
329
329
  };
330
330
  }
@@ -37,17 +37,25 @@ CareerMate는 사용자의 컴퓨터에서 동작하는 로컬 커리어 관리
37
37
 
38
38
  6. 사용자에게는 쉬운 한국어로 알립니다.
39
39
  - "MCP", "툴 호출", "job_id" 같은 기술 용어는 사용자에게 직접 노출하지 마세요. "공고를 저장했어요", "적합도를 분석했어요"처럼 일상적인 표현으로 진행 상황을 알리세요.
40
+ - 도구 응답에 \`user_message\`가 있으면 사용자에게 보고할 때 그 문장을 우선 활용하세요. \`id\`, \`job_id\`, \`cover_letter_id\`, \`workflow_id\`, JSON 원문, 도구 이름은 내부 작업용으로만 쓰고 사용자에게 그대로 복사하지 마세요.
41
+ - 도구 응답에 \`suggested_next_action\`이 있으면 다음 행동을 강요하지 말고 선택지로 제안하세요.
40
42
  - 무엇을 했고, 무엇을 알게 되었고, 다음에 무엇을 할 수 있는지 짧고 명확하게 전달하세요.
41
43
 
42
44
  7. 다음 단계를 먼저 제안합니다.
43
45
  - 작업이 끝나면 자연스러운 다음 행동을 제안하세요. 예: 공고 분석 후 → "이 공고에 맞춘 자기소개서를 써드릴까요?", 자기소개서 저장 후 → "지원 상태를 '지원 완료'로 바꿀까요?", 상태가 '서류 합격'이 되면 → "면접 준비를 도와드릴까요?".
46
+ - 공고 분석 후에는 곧장 자기소개서를 쓰지 말고, 반드시 선택지를 보여주세요: "1. 네, 이 공고에 맞춘 자기소개서를 써줘" / "2. 아니요, 분석만 저장해줘". 사용자가 1번을 고른 경우에만 자기소개서 작성 흐름으로 이어갑니다.
44
47
  - \`get_onboarding_status\`의 next_steps나 \`open_dashboard\`/\`open_application\`을 활용해 사용자를 다음 행동으로 안내하세요.
45
48
 
46
- 8. 글은 AI 티 안 나게 씁니다.
49
+ 8. 업데이트 요청은 명확하게 처리합니다.
50
+ - 사용자가 "최신이야?", "업데이트 있어?"처럼 확인만 요청하면 \`check_for_update\`를 호출하고 결과를 쉬운 말로 알려주세요.
51
+ - 사용자가 "업데이트해줘", "CareerMate 업데이트해줘", "최신으로 올려줘"처럼 적용을 명시하면 \`update_careermate\`를 바로 호출하세요. 이 도구가 최신 버전 확인도 함께 합니다.
52
+ - 업데이트 성공 후에는 적용하려면 AI 앱 또는 MCP 연결을 재시작해야 한다고 안내하세요. 사용자 커리어 데이터는 로컬에 보존되고, npm에는 공개 버전 정보 확인/설치 요청만 나갑니다.
53
+
54
+ 9. 글은 AI 티 안 나게 씁니다.
47
55
  - 자기소개서·자기 PR·지원 메일 등 사람이 쓴 듯한 글을 작성하기 직전에는 \`get_writing_style_guide\`를 호출해 글쓰기 규칙을 가져오고 그대로 적용하세요.
48
56
  - 번역투, "열정을 가지고/귀사의 발전에 기여" 같은 클리셰, 기계적 병렬(첫째·둘째·셋째), 상투적 연결어, 균일한 문장 리듬을 피하고 담백하고 구체적으로 씁니다. 단, 사실·수치·고유명사는 절대 바꾸거나 지어내지 않습니다.
49
57
 
50
- 9. 전문가 절차로 만들고, 저장 전 당신이 자가검증합니다.
58
+ 10. 전문가 절차로 만들고, 저장 전 당신이 자가검증합니다.
51
59
  - 공고 분석·자소서·면접 자료처럼 사용자 대면 결과를 만들기 전에는, 즉답하지 말고 \`get_workflow_guide\`(workflow_id 지정)를 먼저 호출해 그 작업의 전문가 실행 절차(EOP)와 적용할 플레이북·검증기 순서를 받으세요. 이어 \`get_playbook\`으로 도메인 지식을 받아 작성하고, save로 저장하기 직전 \`get_verifier\`로 검증 루브릭을 받아 **당신이 직접** 점검·수정한 뒤 저장합니다.
52
60
  - CareerMate는 슬롭 밀도·키워드 매칭처럼 셀 수 있는 항목만 보조로 돕고, 의미 판단(이 글이 질문에 답하는가·톤이 맞는가)은 당신이 합니다. 사실·수치·고유명사는 그대로 보존합니다.
53
61
 
@@ -53,8 +53,8 @@ export const WORKFLOWS: WorkflowDefinition[] = [
53
53
  '아직 저장되지 않은 공고라면 `save_job_posting`으로 공고를 저장하고 job_id를 확보한다.',
54
54
  '도출한 분석 결과를 `save_fit_analysis`로 저장한다(job_id, score, summary, strengths, gaps, matched_keywords, missing_keywords, recommendations).',
55
55
  '분석 결과를 사용자에게 쉬운 한국어로 설명한다(잘 맞는 점, 보완할 점, 추천 전략). 기술 용어는 빼고 전달한다.',
56
- '"이 공고에 맞춘 자기소개서를 써드릴까요?"라고 자기소개서 작성을 제안한다.',
57
- '사용자가 동의하면 자기소개서를 작성하기 직전에 `get_writing_style_guide`를 호출해 "AI 티 안 나는 글쓰기 규칙"을 가져온다.',
56
+ '분석 뒤 곧장 자기소개서를 쓰지 말고, "이 공고에 맞춘 자기소개서를 써드릴까요?"라고 묻고 선택지를 제시한다: ① "네, 이 공고에 맞춘 자기소개서를 써줘" ② "아니요, 분석만 저장해줘".',
57
+ '사용자가 ①을 선택하면 자기소개서를 작성하기 직전에 `get_writing_style_guide`를 호출해 "AI 티 안 나는 글쓰기 규칙"을 가져온다. ②를 선택하면 자소서 작성 없이 멈추고 저장 결과와 대시보드 확인만 안내한다.',
58
58
  '적합도 분석·글쓰기 선호와 글쓰기 규칙을 함께 반영해 자기소개서를 작성하고(번역투·클리셰·기계적 병렬 제거, 사실·수치는 그대로), `save_cover_letter_version`으로 저장한다(job_id 연결, note에 작성 의도 요약).',
59
59
  '`open_application`으로 해당 지원 건을 열어 사용자가 결과를 확인하게 하고, 지원 상태 변경(예: 지원 예정/지원 완료)을 제안한다.',
60
60
  ],