careermate 0.0.5 → 0.0.7
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 +13 -10
- package/INSTALL.md +16 -15
- package/README.md +24 -24
- package/THIRD_PARTY_NOTICES.md +2 -3
- package/bin/careermate.mjs +8 -1
- package/dist/career-os/README.md +12 -6
- package/dist/career-os/eop/career-description.md +139 -0
- package/dist/career-os/eop/cover-letter.md +10 -5
- package/dist/career-os/eop/fit-analysis.md +16 -6
- package/dist/career-os/eop/interview-prep.md +209 -21
- package/dist/career-os/eop/job-analysis.md +6 -4
- package/dist/career-os/eop/profile-extraction.md +5 -4
- package/dist/career-os/knowledge/cover-letter.md +5 -4
- package/dist/career-os/knowledge/fit-matching.md +8 -8
- package/dist/career-os/knowledge/human-writing.md +2 -0
- package/dist/career-os/knowledge/interview-behavioral.md +76 -8
- package/dist/career-os/knowledge/interview-technical.md +106 -4
- package/dist/career-os/knowledge/linkedin-profile.md +1 -1
- package/dist/career-os/knowledge/networking-referrals.md +1 -1
- package/dist/career-os/knowledge/offer-evaluation-decision.md +10 -10
- package/dist/career-os/knowledge/portfolio.md +3 -3
- package/dist/career-os/knowledge/verifiers/ats-compat.md +8 -4
- package/dist/career-os/knowledge/verifiers/human-voice.md +4 -2
- package/dist/career-os/knowledge/verifiers/recency-staleness.md +1 -1
- package/dist/career-os/knowledge/verifiers/responsiveness-on-target.md +2 -2
- package/dist/career-os/knowledge/verifiers/truthfulness.md +1 -1
- package/dist/doctor.mjs +692 -71
- package/dist/mcp.mjs +710 -71
- package/dist/migrate.mjs +52 -1
- package/dist/public/app.js +42 -16
- package/dist/public/i18n/en/applications.js +23 -0
- package/dist/public/i18n/en/common.js +22 -0
- package/dist/public/i18n/en/documents.js +133 -0
- package/dist/public/i18n/en/home.js +75 -0
- package/dist/public/i18n/en/interview.js +69 -0
- package/dist/public/i18n/en/jobs.js +152 -0
- package/dist/public/i18n/en/nav.js +18 -0
- package/dist/public/i18n/en/profile.js +201 -0
- package/dist/public/i18n/en/settings.js +127 -0
- package/dist/public/i18n/en/status.js +22 -0
- package/dist/public/i18n/en.js +17 -0
- package/dist/public/i18n/ko/applications.js +23 -0
- package/dist/public/i18n/ko/common.js +22 -0
- package/dist/public/i18n/ko/documents.js +133 -0
- package/dist/public/i18n/ko/home.js +63 -0
- package/dist/public/i18n/ko/interview.js +69 -0
- package/dist/public/i18n/ko/jobs.js +152 -0
- package/dist/public/i18n/ko/nav.js +18 -0
- package/dist/public/i18n/ko/profile.js +201 -0
- package/dist/public/i18n/ko/settings.js +127 -0
- package/dist/public/i18n/ko/status.js +23 -0
- package/dist/public/i18n/ko.js +18 -0
- package/dist/public/i18n.js +55 -0
- package/dist/public/index.html +1 -0
- package/dist/public/lib.js +24 -17
- package/dist/public/pages/applications.js +17 -21
- package/dist/public/pages/documents.js +153 -107
- package/dist/public/pages/home.js +88 -53
- package/dist/public/pages/interview.js +105 -51
- package/dist/public/pages/jobs.js +330 -106
- package/dist/public/pages/profile.js +157 -152
- package/dist/public/pages/settings.js +154 -102
- package/dist/public/styles.css +28 -1
- package/dist/web.mjs +495 -75
- package/docs/FAQ.md +23 -3
- package/docs/MCP_TOOLS.md +22 -13
- package/docs/START_WORKFLOW.md +3 -3
- package/docs/SUPPORTED_AI_APPS.md +13 -0
- package/docs/WORKFLOWS.md +77 -20
- package/docs/career-os/README.md +12 -6
- package/docs/career-os/eop/career-description.md +139 -0
- package/docs/career-os/eop/cover-letter.md +10 -5
- package/docs/career-os/eop/fit-analysis.md +16 -6
- package/docs/career-os/eop/interview-prep.md +209 -21
- package/docs/career-os/eop/job-analysis.md +6 -4
- package/docs/career-os/eop/profile-extraction.md +5 -4
- package/docs/career-os/knowledge/cover-letter.md +5 -4
- package/docs/career-os/knowledge/fit-matching.md +8 -8
- package/docs/career-os/knowledge/human-writing.md +2 -0
- package/docs/career-os/knowledge/interview-behavioral.md +76 -8
- package/docs/career-os/knowledge/interview-technical.md +106 -4
- package/docs/career-os/knowledge/linkedin-profile.md +1 -1
- package/docs/career-os/knowledge/networking-referrals.md +1 -1
- package/docs/career-os/knowledge/offer-evaluation-decision.md +10 -10
- package/docs/career-os/knowledge/portfolio.md +3 -3
- package/docs/career-os/knowledge/verifiers/ats-compat.md +8 -4
- package/docs/career-os/knowledge/verifiers/human-voice.md +4 -2
- package/docs/career-os/knowledge/verifiers/recency-staleness.md +1 -1
- package/docs/career-os/knowledge/verifiers/responsiveness-on-target.md +2 -2
- package/docs/career-os/knowledge/verifiers/truthfulness.md +1 -1
- package/package.json +2 -2
- package/packages/core/src/onboarding.ts +2 -1
- package/packages/core/src/services.ts +134 -17
- package/packages/core/src/summary.ts +59 -1
- package/packages/core/src/verify/lint.ts +4 -0
- package/packages/db/src/index.ts +1 -0
- package/packages/db/src/repositories.ts +0 -0
- package/packages/db/src/schema.ts +17 -0
- package/packages/exporters/src/index.ts +28 -17
- package/packages/knowledge/src/index.ts +55 -9
- package/packages/mcp-tools/src/tools.ts +250 -24
- package/packages/mcp-tools/src/update.ts +30 -15
- package/packages/parsers/src/documents.ts +12 -1
- package/packages/prompts/src/humanize.ts +46 -0
- package/packages/prompts/src/index.ts +5 -1
- package/packages/prompts/src/install.ts +4 -4
- package/packages/prompts/src/onboarding.ts +4 -4
- package/packages/prompts/src/system.ts +9 -7
- package/packages/shared/src/enums.ts +14 -4
- package/packages/shared/src/schemas.ts +41 -0
- package/packages/workflows/src/definitions.ts +45 -8
- package/scripts/init.ts +284 -13
- package/dist/site/analytics.js +0 -97
- package/dist/site/favicon.svg +0 -1
- package/dist/site/fonts/PretendardVariable.woff2 +0 -0
- package/dist/site/icon-180.png +0 -0
- package/dist/site/icon-512.png +0 -0
- package/dist/site/index.html +0 -465
- package/dist/site/landing.js +0 -163
- package/dist/site/llms-install.txt +0 -218
- package/dist/site/robots.txt +0 -4
- package/dist/site/sitemap.xml +0 -21
- package/dist/site/start.html +0 -356
- package/dist/site/style.css +0 -665
- package/dist/site/vercel.json +0 -20
|
@@ -58,12 +58,13 @@ export const VERIFIER_IDS = [
|
|
|
58
58
|
] as const;
|
|
59
59
|
export type VerifierId = (typeof VERIFIER_IDS)[number];
|
|
60
60
|
|
|
61
|
-
/**
|
|
61
|
+
/** Per-feature execution procedures — docs/career-os/eop/<feature>.md. */
|
|
62
62
|
export const EOP_FEATURES = [
|
|
63
63
|
'fit-analysis',
|
|
64
64
|
'job-analysis',
|
|
65
65
|
'profile-extraction',
|
|
66
66
|
'cover-letter',
|
|
67
|
+
'career-description',
|
|
67
68
|
'interview-prep',
|
|
68
69
|
] as const;
|
|
69
70
|
export type EopFeature = (typeof EOP_FEATURES)[number];
|
|
@@ -95,7 +96,7 @@ export function getEop(feature: EopFeature): string {
|
|
|
95
96
|
|
|
96
97
|
/** A goal → which EOP, expert playbooks, and verifiers to apply, in order. */
|
|
97
98
|
export interface CareerRoute {
|
|
98
|
-
eop?: EopFeature;
|
|
99
|
+
eop?: EopFeature | EopFeature[];
|
|
99
100
|
expertSequence: ExpertDomain[];
|
|
100
101
|
verifierSequence: VerifierId[];
|
|
101
102
|
loop?: 'draft_verify_revise';
|
|
@@ -109,23 +110,65 @@ export const CAREER_ROUTES: Record<string, CareerRoute> = {
|
|
|
109
110
|
verifierSequence: ['truthfulness', 'consistency'],
|
|
110
111
|
},
|
|
111
112
|
analyze_job: {
|
|
112
|
-
eop: 'job-analysis',
|
|
113
|
+
eop: ['job-analysis', 'fit-analysis'],
|
|
113
114
|
expertSequence: ['fit-matching', 'company-research'],
|
|
114
115
|
verifierSequence: ['truthfulness', 'consistency', 'responsiveness-on-target'],
|
|
115
116
|
loop: 'draft_verify_revise',
|
|
116
117
|
},
|
|
118
|
+
// R22(audit): responsiveness-on-target는 한국 문항형 자소서 전용 검증기(문항별 1:1 응답·
|
|
119
|
+
// 글자수 상·하한 = "핵심 탈락 사유")인데 analyze_job에만 라우팅돼 있어 정작 자소서 자가검증
|
|
120
|
+
// 루프에서 빠져 있었다. 동문서답·미응답 문항·글자수 초과를 잡도록 write_cover_letter에 추가.
|
|
117
121
|
write_cover_letter: {
|
|
118
122
|
eop: 'cover-letter',
|
|
119
123
|
expertSequence: ['cover-letter', 'human-writing', 'company-research'],
|
|
120
|
-
verifierSequence: ['human-voice', 'truthfulness', 'ats-compat', 'consistency'],
|
|
124
|
+
verifierSequence: ['human-voice', 'truthfulness', 'responsiveness-on-target', 'ats-compat', 'consistency'],
|
|
125
|
+
loop: 'draft_verify_revise',
|
|
126
|
+
},
|
|
127
|
+
write_career_description: {
|
|
128
|
+
eop: 'career-description',
|
|
129
|
+
expertSequence: ['resume', 'ats'],
|
|
130
|
+
verifierSequence: ['truthfulness', 'consistency', 'ats-compat'],
|
|
121
131
|
loop: 'draft_verify_revise',
|
|
122
132
|
},
|
|
123
133
|
prepare_interview: {
|
|
124
134
|
eop: 'interview-prep',
|
|
125
|
-
expertSequence: ['interview-behavioral', 'interview-technical', 'company-research'],
|
|
126
|
-
|
|
135
|
+
expertSequence: ['recruiter-screen', 'interview-behavioral', 'interview-technical', 'company-research'],
|
|
136
|
+
// R22(audit): recency-staleness 추가 — 면접의 "최근 소식"·회사 근황이 낡으면 거짓 신선도가
|
|
137
|
+
// 되므로(company-research) 저장 전 신선도 점검을 푸시한다.
|
|
138
|
+
verifierSequence: ['truthfulness', 'consistency', 'recency-staleness'],
|
|
127
139
|
loop: 'draft_verify_revise',
|
|
128
140
|
},
|
|
141
|
+
// R7 A2: manage_application_status had no route → rejection-triage / offer-evaluation /
|
|
142
|
+
// salary-negotiation were orphaned (served only on-demand via get_playbook). Route the
|
|
143
|
+
// decision-stage playbooks so they are pushed when the user manages application outcomes.
|
|
144
|
+
// R7 A4: A2 routed the decision-stage playbooks, but the EOP-free lifecycle domains
|
|
145
|
+
// onboarding-first-90-days and networking-referrals were still on-demand-only. Anchor them
|
|
146
|
+
// here (the only EOP-free lifecycle route). NOTE: networking's strongest lever is *before*
|
|
147
|
+
// applied (pre-referral, networking-referrals §2) — this route is the surfacing point, not
|
|
148
|
+
// the recommended timing, so surface networking early when the user is pre-application.
|
|
149
|
+
// linkedin-profile/portfolio stay on-demand pending a dedicated personal-brand workflow id
|
|
150
|
+
// (FIXES.md / STATUS §7.9 follow-up — onboarding EOP is résumé-import-only, so brand-
|
|
151
|
+
// creation playbooks would blur its stage meaning). Additive wiring (A1/A2 precedent).
|
|
152
|
+
manage_application_status: {
|
|
153
|
+
expertSequence: [
|
|
154
|
+
'rejection-triage-iteration',
|
|
155
|
+
'offer-evaluation-decision',
|
|
156
|
+
'salary-negotiation',
|
|
157
|
+
'onboarding-first-90-days',
|
|
158
|
+
'networking-referrals',
|
|
159
|
+
],
|
|
160
|
+
// R22(audit): recency-staleness 추가 — 연봉 벤치마크·오퍼 비교는 시점이 지나면 낡은 기준이
|
|
161
|
+
// 되므로(salary-negotiation/offer-evaluation) 저장 전 신선도 점검을 푸시한다.
|
|
162
|
+
verifierSequence: ['truthfulness', 'consistency', 'recency-staleness'],
|
|
163
|
+
},
|
|
164
|
+
// R7 후속: linkedin-profile·portfolio는 onboarding(이력서 임포트)과 단계 의미가
|
|
165
|
+
// 달라(개인 브랜드 자산 *생성*) 전용 워크플로우로 분리(consensus 권고). networking은
|
|
166
|
+
// manage_application_status에 이미 정박. 저장은 add_resume(kind=portfolio); linkedin
|
|
167
|
+
// 전용 저장/검증은 Phase B.
|
|
168
|
+
build_personal_brand: {
|
|
169
|
+
expertSequence: ['linkedin-profile', 'portfolio'],
|
|
170
|
+
verifierSequence: ['truthfulness', 'human-voice', 'consistency'],
|
|
171
|
+
},
|
|
129
172
|
};
|
|
130
173
|
|
|
131
174
|
export function getRoute(id: string): CareerRoute | undefined {
|
|
@@ -142,12 +185,15 @@ export function renderRouteGuide(id: string): string | undefined {
|
|
|
142
185
|
const route = CAREER_ROUTES[id];
|
|
143
186
|
if (!route) return undefined;
|
|
144
187
|
const lines: string[] = [];
|
|
145
|
-
|
|
188
|
+
const eops = route.eop ? (Array.isArray(route.eop) ? route.eop : [route.eop]) : [];
|
|
189
|
+
if (eops.length) {
|
|
146
190
|
lines.push('---', '');
|
|
147
191
|
lines.push('## 전문가 실행 절차 (EOP) — 즉답하지 말고 이 절차를 따르세요');
|
|
148
192
|
lines.push('');
|
|
149
|
-
|
|
150
|
-
|
|
193
|
+
for (const e of eops) {
|
|
194
|
+
lines.push(getEop(e));
|
|
195
|
+
lines.push('');
|
|
196
|
+
}
|
|
151
197
|
}
|
|
152
198
|
lines.push('## 이 작업에 적용할 전문가 플레이북');
|
|
153
199
|
lines.push('작성 직전, 아래 도메인의 깊은 지식을 받아 적용하세요:');
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
JobInputSchema,
|
|
22
22
|
FitAnalysisInputSchema,
|
|
23
23
|
CoverLetterVersionInputSchema,
|
|
24
|
+
ApplicationSubmissionSchema,
|
|
24
25
|
InterviewPrepInputSchema,
|
|
25
26
|
DocumentInputSchema,
|
|
26
27
|
APPLICATION_STATUSES,
|
|
@@ -63,7 +64,7 @@ import {
|
|
|
63
64
|
previewCoverLetter,
|
|
64
65
|
summarizeReport,
|
|
65
66
|
} from '@careermate/core';
|
|
66
|
-
import {
|
|
67
|
+
import { getWritingStyleGuide } from '@careermate/prompts';
|
|
67
68
|
import { getWorkflow, renderWorkflowMarkdown, WORKFLOWS } from '@careermate/workflows';
|
|
68
69
|
import {
|
|
69
70
|
getPlaybook,
|
|
@@ -75,7 +76,14 @@ import {
|
|
|
75
76
|
type ExpertDomain,
|
|
76
77
|
type VerifierId,
|
|
77
78
|
} from '@careermate/knowledge';
|
|
78
|
-
import {
|
|
79
|
+
import {
|
|
80
|
+
coverLetterToMarkdown,
|
|
81
|
+
coverLetterToHtml,
|
|
82
|
+
resumeToMarkdown,
|
|
83
|
+
resumeToHtml,
|
|
84
|
+
profileToMarkdown,
|
|
85
|
+
profileToHtml,
|
|
86
|
+
} from '@careermate/exporters';
|
|
79
87
|
import { extractDocument } from '@careermate/parsers';
|
|
80
88
|
import { ok, fail, type ToolDef } from './result.ts';
|
|
81
89
|
import { resolveDashboardUrl, openInBrowser, openInFileManager } from './bridge.ts';
|
|
@@ -93,15 +101,28 @@ function listInboxFiles(dir: string): { name: string; path: string }[] {
|
|
|
93
101
|
} catch {
|
|
94
102
|
return [];
|
|
95
103
|
}
|
|
104
|
+
// Resolve the inbox itself once so we can confine listed files to it: a symlink
|
|
105
|
+
// dropped in the inbox whose real target escapes the inbox (e.g. → ~/.ssh/id_rsa
|
|
106
|
+
// or a private doc elsewhere) must not be listed or later read by read_inbox.
|
|
107
|
+
let realDir: string;
|
|
108
|
+
try {
|
|
109
|
+
realDir = fs.realpathSync.native(dir);
|
|
110
|
+
} catch {
|
|
111
|
+
realDir = dir;
|
|
112
|
+
}
|
|
96
113
|
const SKIP = /^[.~]|^(thumbs\.db|desktop\.ini)$/i;
|
|
97
114
|
const out: { name: string; path: string }[] = [];
|
|
98
115
|
for (const name of names) {
|
|
99
116
|
if (SKIP.test(name)) continue;
|
|
100
117
|
const p = path.join(dir, name);
|
|
101
118
|
try {
|
|
102
|
-
if (fs.statSync(p).isFile())
|
|
119
|
+
if (!fs.statSync(p).isFile()) continue;
|
|
120
|
+
const real = fs.realpathSync.native(p);
|
|
121
|
+
const rel = path.relative(realDir, real);
|
|
122
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) continue; // escapes the inbox — skip
|
|
123
|
+
out.push({ name, path: p });
|
|
103
124
|
} catch {
|
|
104
|
-
/* unreadable entry — skip */
|
|
125
|
+
/* unreadable entry / broken link — skip */
|
|
105
126
|
}
|
|
106
127
|
}
|
|
107
128
|
return out;
|
|
@@ -125,13 +146,15 @@ async function mapWithLimit<T, R>(items: T[], limit: number, fn: (item: T, index
|
|
|
125
146
|
return out;
|
|
126
147
|
}
|
|
127
148
|
|
|
128
|
-
/** Statuses at/after which interview prep is
|
|
149
|
+
/** Statuses at/after which interview prep is recommended as a next step. */
|
|
129
150
|
const INTERVIEW_UNLOCKED = ['document_passed', 'interview', 'final_passed'];
|
|
130
151
|
|
|
131
152
|
/**
|
|
132
153
|
* Pick the recommended Career-OS route from the loaded context, so the AI is
|
|
133
154
|
* nudged onto the expert procedure for what it's most likely about to do.
|
|
134
|
-
* Returns a get_workflow_guide workflow_id, or null when
|
|
155
|
+
* Returns a get_workflow_guide workflow_id for job-centered tasks, or null when
|
|
156
|
+
* there's no target job. Non-job tasks such as write_career_description are
|
|
157
|
+
* selected from the user's explicit request via get_workflow_guide.
|
|
135
158
|
*/
|
|
136
159
|
function pickRoute(ctx: ReturnType<typeof getApplicationContext>): string | null {
|
|
137
160
|
if (!ctx.job) return null;
|
|
@@ -190,9 +213,9 @@ export const TOOLS: ToolDef[] = [
|
|
|
190
213
|
/* --------------------------------------------------------- inbox (파일 인입) */
|
|
191
214
|
{
|
|
192
215
|
name: 'open_inbox',
|
|
193
|
-
title: '
|
|
216
|
+
title: '문서 인입 폴더 열기',
|
|
194
217
|
description:
|
|
195
|
-
'CLI 클라이언트는 파일 첨부가 안 되므로, 사용자가 기존
|
|
218
|
+
'CLI 클라이언트는 파일 첨부가 안 되므로, 사용자가 기존 이력서·경력기술서·포트폴리오·자기소개서 파일(hwp/hwpx/docx/pdf 등)을 끌어다 넣을 수 있는 로컬 인입 폴더(~/.careermate/inbox)를 보장하고 파일 탐색기로 엽니다. 온보딩에서 "기존에 작성해 둔 문서 파일이 있다"고 사용자가 답했을 때만 호출하세요 — 묻지도 않고 폴더를 자동으로 열지 마세요. 파일 경로를 직접 알려주는 사용자에게는 이 도구 대신 read_document를 쓰세요. 호출하면 폴더 경로와 현재 폴더에 들어 있는 파일 목록을 돌려줍니다. 사용자가 파일을 다 넣었다고 하면 read_inbox로 본문을 읽으세요.',
|
|
196
219
|
inputSchema: {},
|
|
197
220
|
handler: () => {
|
|
198
221
|
const dir = getInboxDir();
|
|
@@ -200,7 +223,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
200
223
|
const files = listInboxFiles(dir);
|
|
201
224
|
const list = files.length ? files.map((f) => `- ${f.name}`).join('\n') : '(아직 비어 있음)';
|
|
202
225
|
return ok(
|
|
203
|
-
|
|
226
|
+
`문서 인입 폴더를 열었습니다:\n${dir}\n\n여기에 이력서·경력기술서·포트폴리오·기존 자기소개서 파일을 넣고 "다 넣었어"라고 알려주세요. (hwp·hwpx·docx·pdf는 자동으로 텍스트를 추출하고, 텍스트 레이어가 없는 이미지·스캔 PDF만 제가 직접 읽습니다.)\n\n현재 파일:\n${list}`,
|
|
204
227
|
{ dir, files },
|
|
205
228
|
);
|
|
206
229
|
},
|
|
@@ -209,7 +232,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
209
232
|
name: 'read_inbox',
|
|
210
233
|
title: '인입 폴더 파일 읽기',
|
|
211
234
|
description:
|
|
212
|
-
'인입 폴더(~/.careermate/inbox)에 사용자가 넣어 둔 문서들의 본문을 한 번에 읽어 돌려줍니다. open_inbox로 폴더를 안내한 뒤 사용자가 "다 넣었어"라고 하면 호출하세요. docx
|
|
235
|
+
'인입 폴더(~/.careermate/inbox)에 사용자가 넣어 둔 문서들의 본문을 한 번에 읽어 돌려줍니다. open_inbox로 폴더를 안내한 뒤 사용자가 "다 넣었어"라고 하면 호출하세요. docx·hwp·hwpx·pdf·텍스트(.txt/.md)에서 본문 텍스트를 추출해 돌려줍니다(read_document와 동일한 추출 스택). 단 텍스트 레이어가 없는 이미지·스캔 PDF는 추출되지 않아(unsupported=true) 파일 경로만 돌려주니, 그 경로는 AI 클라이언트의 파일 읽기 기능으로 직접 열어 읽으세요. 읽은 내용을 구조화해 프로필은 save_profile, 이력서·경력기술서·포트폴리오는 add_resume, 기존 자기소개서는 save_cover_letter_version으로 저장하세요(파일 내용을 새 파일로 다시 쓰지 마세요). filename을 주면 해당 파일만 읽습니다.',
|
|
213
236
|
inputSchema: {
|
|
214
237
|
filename: z.string().optional().describe('특정 파일만 읽을 때 파일명(대소문자 무시). 생략하면 폴더 안 모든 파일을 읽습니다.'),
|
|
215
238
|
max_chars: z.number().optional().describe('파일별 반환 텍스트 최대 길이(초과 시 잘라냄). 기본 20000자'),
|
|
@@ -252,7 +275,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
252
275
|
)
|
|
253
276
|
.join('\n');
|
|
254
277
|
return ok(
|
|
255
|
-
`인입 폴더 파일 ${files.length}건을 읽었습니다:\n${summary}\n\n추출된 내용을 구조화해 save_profile
|
|
278
|
+
`인입 폴더 파일 ${files.length}건을 읽었습니다:\n${summary}\n\n추출된 내용을 구조화해 프로필은 save_profile, 이력서·경력기술서·포트폴리오는 add_resume, 기존 자기소개서는 save_cover_letter_version으로 저장하세요.`,
|
|
256
279
|
{ dir, files },
|
|
257
280
|
);
|
|
258
281
|
},
|
|
@@ -325,7 +348,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
325
348
|
name: 'add_resume',
|
|
326
349
|
title: '이력서/경력기술서 추가',
|
|
327
350
|
description:
|
|
328
|
-
'이력서, 경력기술서, 포트폴리오 등 문서를 저장합니다. content에는 사용자가 업로드/붙여넣은 텍스트
|
|
351
|
+
'이력서, 경력기술서, 포트폴리오 등 문서를 저장합니다. content에는 사용자가 업로드/붙여넣은 텍스트 또는 AI가 전문가 절차로 정리한 Markdown을 넣으세요. 경력기술서를 작성해 저장할 때는 kind=career_description, source=ai를 사용합니다. ⚠️ content에는 이 문서 한 종류의 본문만 넣고, 자기소개서 등 다른 종류 문서를 같은 content에 이어붙이지 마세요 — 자기소개서는 save_cover_letter_version으로 따로 저장합니다(한 번에 둘 다 요청받았어도 한 저장 호출에 두 문서를 합치지 않습니다). kind 기본값은 resume입니다. is_primary=true로 대표 문서를 지정하면 get_application_context의 primary_resume로 노출됩니다.',
|
|
329
352
|
// Reuse the shared (length-bounded, whitespace-rejecting) Document schema so
|
|
330
353
|
// the MCP ingestion path — the primary route for résumé text — inherits the
|
|
331
354
|
// same caps as the web API. kind stays optional here (defaults to resume).
|
|
@@ -349,6 +372,24 @@ export const TOOLS: ToolDef[] = [
|
|
|
349
372
|
return ok(`문서 ${docs.length}건`, docs);
|
|
350
373
|
},
|
|
351
374
|
},
|
|
375
|
+
{
|
|
376
|
+
name: 'delete_resume',
|
|
377
|
+
title: '이력서/문서 삭제',
|
|
378
|
+
description:
|
|
379
|
+
'저장된 이력서·경력기술서·포트폴리오 등 문서를 삭제합니다. 사용자가 명확히 삭제를 요청했을 때만 사용하세요. 먼저 get_resumes로 목록을 확인하고 어떤 문서인지 사용자와 확인한 뒤 호출합니다. confirm에는 DELETE를 넣으세요.',
|
|
380
|
+
inputSchema: {
|
|
381
|
+
document_id: z.string(),
|
|
382
|
+
confirm: z.string().describe('삭제 확인값. 정확히 DELETE를 넣어야 삭제됩니다.'),
|
|
383
|
+
},
|
|
384
|
+
handler: (args) => {
|
|
385
|
+
if (args?.confirm !== DELETE_CONFIRMATION) return fail('삭제하려면 confirm에 DELETE를 넣어 다시 호출하세요.');
|
|
386
|
+
const doc = documentRepo.get(args.document_id);
|
|
387
|
+
if (!doc) return fail('문서를 찾을 수 없습니다.');
|
|
388
|
+
if (!documentRepo.remove(doc.id)) return fail('문서를 삭제하지 못했습니다.');
|
|
389
|
+
const user_message = `'${doc.title}' 문서를 삭제했어요.`;
|
|
390
|
+
return ok(user_message, { deleted: true, document_id: doc.id, user_message });
|
|
391
|
+
},
|
|
392
|
+
},
|
|
352
393
|
|
|
353
394
|
/* ----------------------------------------------------------- experiences */
|
|
354
395
|
{
|
|
@@ -424,6 +465,60 @@ export const TOOLS: ToolDef[] = [
|
|
|
424
465
|
return ok(`기술 ${list.length}건`, list);
|
|
425
466
|
},
|
|
426
467
|
},
|
|
468
|
+
{
|
|
469
|
+
name: 'delete_experience',
|
|
470
|
+
title: '경력 항목 삭제',
|
|
471
|
+
description:
|
|
472
|
+
'구조화된 경력(직장) 항목 하나를 삭제합니다. 잘못 추출/입력된 경력을 지울 때 사용하세요. 먼저 get_experiences로 목록과 id를 확인하고 사용자와 확인한 뒤 호출합니다. (내용 수정은 add_experience로 같은 항목을 다시 저장하면 갱신됩니다.) confirm에는 DELETE를 넣으세요.',
|
|
473
|
+
inputSchema: {
|
|
474
|
+
experience_id: z.string(),
|
|
475
|
+
confirm: z.string().describe('삭제 확인값. 정확히 DELETE를 넣어야 삭제됩니다.'),
|
|
476
|
+
},
|
|
477
|
+
handler: (args) => {
|
|
478
|
+
if (args?.confirm !== DELETE_CONFIRMATION) return fail('삭제하려면 confirm에 DELETE를 넣어 다시 호출하세요.');
|
|
479
|
+
const exp = experienceRepo.get(args.experience_id);
|
|
480
|
+
if (!exp) return fail('해당 경력 항목을 찾을 수 없습니다.');
|
|
481
|
+
if (!experienceRepo.remove(exp.id)) return fail('경력 항목을 삭제하지 못했습니다.');
|
|
482
|
+
const user_message = `'${exp.company}' 경력을 삭제했어요.`;
|
|
483
|
+
return ok(user_message, { deleted: true, experience_id: exp.id, user_message });
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
name: 'delete_project',
|
|
488
|
+
title: '프로젝트 항목 삭제',
|
|
489
|
+
description:
|
|
490
|
+
'구조화된 프로젝트 항목 하나를 삭제합니다. 잘못 추출/입력된 프로젝트를 지울 때 사용하세요. 먼저 get_projects로 목록과 id를 확인하고 사용자와 확인한 뒤 호출합니다. (내용 수정은 add_project로 같은 항목을 다시 저장하면 갱신됩니다.) confirm에는 DELETE를 넣으세요.',
|
|
491
|
+
inputSchema: {
|
|
492
|
+
project_id: z.string(),
|
|
493
|
+
confirm: z.string().describe('삭제 확인값. 정확히 DELETE를 넣어야 삭제됩니다.'),
|
|
494
|
+
},
|
|
495
|
+
handler: (args) => {
|
|
496
|
+
if (args?.confirm !== DELETE_CONFIRMATION) return fail('삭제하려면 confirm에 DELETE를 넣어 다시 호출하세요.');
|
|
497
|
+
const proj = projectRepo.get(args.project_id);
|
|
498
|
+
if (!proj) return fail('해당 프로젝트 항목을 찾을 수 없습니다.');
|
|
499
|
+
if (!projectRepo.remove(proj.id)) return fail('프로젝트 항목을 삭제하지 못했습니다.');
|
|
500
|
+
const user_message = `'${proj.name}' 프로젝트를 삭제했어요.`;
|
|
501
|
+
return ok(user_message, { deleted: true, project_id: proj.id, user_message });
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
name: 'delete_skill',
|
|
506
|
+
title: '기술스택 항목 삭제',
|
|
507
|
+
description:
|
|
508
|
+
'구조화된 기술스택 항목 하나를 삭제합니다. 잘못 추출/입력된 기술을 지울 때 사용하세요. 먼저 get_skills로 목록과 id를 확인하고 사용자와 확인한 뒤 호출합니다. (내용 수정은 add_skill로 같은 항목을 다시 저장하면 갱신됩니다.) confirm에는 DELETE를 넣으세요.',
|
|
509
|
+
inputSchema: {
|
|
510
|
+
skill_id: z.string(),
|
|
511
|
+
confirm: z.string().describe('삭제 확인값. 정확히 DELETE를 넣어야 삭제됩니다.'),
|
|
512
|
+
},
|
|
513
|
+
handler: (args) => {
|
|
514
|
+
if (args?.confirm !== DELETE_CONFIRMATION) return fail('삭제하려면 confirm에 DELETE를 넣어 다시 호출하세요.');
|
|
515
|
+
const skill = skillRepo.get(args.skill_id);
|
|
516
|
+
if (!skill) return fail('해당 기술 항목을 찾을 수 없습니다.');
|
|
517
|
+
if (!skillRepo.remove(skill.id)) return fail('기술 항목을 삭제하지 못했습니다.');
|
|
518
|
+
const user_message = `'${skill.name}' 기술을 삭제했어요.`;
|
|
519
|
+
return ok(user_message, { deleted: true, skill_id: skill.id, user_message });
|
|
520
|
+
},
|
|
521
|
+
},
|
|
427
522
|
|
|
428
523
|
/* ---------------------------------------------------------- cover letters */
|
|
429
524
|
{
|
|
@@ -441,7 +536,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
441
536
|
name: 'save_cover_letter_version',
|
|
442
537
|
title: '자기소개서 버전 저장',
|
|
443
538
|
description:
|
|
444
|
-
'자기소개서의 새 버전을 저장합니다. CareerMate 자소서 작성 워크플로우의 핵심 저장 단계입니다. cover_letter_id를 주면 기존 자소서에 새 버전을 추가하고, 없으면 새 자소서를 만들어 v1로 저장합니다(이때 title 권장). note에 이 버전의 변경 요약(예: "지원동기 보강")을 남기면 사용자가 대시보드에서 버전 히스토리를 이해하기 쉽습니다. job_id로 공고에 연결하면 지원 항목과 자동 연결됩니다. 저장 직전 자동으로 "근거 없는 수치" 점검을 돌립니다 — 본문의 정량 수치가 저장된 경력·이력서·프로젝트에 근거가 없으면(환각 의심) 저장이 막힙니다. 그때는 원본의 실제 수치로 고치거나, 의도한 값이면 force:true로 다시 저장하세요.',
|
|
539
|
+
'자기소개서의 새 버전을 저장합니다. CareerMate 자소서 작성 워크플로우의 핵심 저장 단계입니다. ⚠️ content에는 자기소개서 본문만 넣고, 경력기술서 등 다른 종류 문서를 이어붙이지 마세요(경력기술서는 add_resume(kind=career_description)로 따로 저장). cover_letter_id를 주면 기존 자소서에 새 버전을 추가하고, 없으면 새 자소서를 만들어 v1로 저장합니다(이때 title 권장). note에 이 버전의 변경 요약(예: "지원동기 보강")을 남기면 사용자가 대시보드에서 버전 히스토리를 이해하기 쉽습니다. job_id로 공고에 연결하면 지원 항목과 자동 연결됩니다. 저장 직전 자동으로 "근거 없는 수치" 점검을 돌립니다 — 본문의 정량 수치가 저장된 경력·이력서·프로젝트에 근거가 없으면(환각 의심) 저장이 막힙니다. 그때는 원본의 실제 수치로 고치거나, 의도한 값이면 force:true로 다시 저장하세요.',
|
|
445
540
|
inputSchema: CoverLetterVersionInputSchema.shape,
|
|
446
541
|
handler: (args) => {
|
|
447
542
|
try {
|
|
@@ -513,7 +608,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
513
608
|
{
|
|
514
609
|
name: 'get_job_posting',
|
|
515
610
|
title: '채용공고 상세 조회',
|
|
516
|
-
description: '공고 1건의 상세를 조회합니다. 공고 정보와 함께 적합도 분석, 지원 상태,
|
|
611
|
+
description: '공고 1건의 상세를 조회합니다. 공고 정보와 함께 적합도 분석, 지원 상태, 자기소개서, 면접 준비 자료를 한 번에 돌려줍니다.',
|
|
517
612
|
inputSchema: { job_id: z.string() },
|
|
518
613
|
readOnly: true,
|
|
519
614
|
handler: (args) => {
|
|
@@ -586,13 +681,48 @@ export const TOOLS: ToolDef[] = [
|
|
|
586
681
|
// 여기에 추천 경로를 실어 전문가 절차로 유도한다. 스키마 변경 없이 반환 JSON만 확장.
|
|
587
682
|
const recommended_route = pickRoute(ctx);
|
|
588
683
|
const route = recommended_route ? getRoute(recommended_route) : undefined;
|
|
684
|
+
// 오늘 날짜 주입: LLM은 현재 날짜를 신뢰성 있게 모른다.
|
|
685
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
686
|
+
// 재직기간/연차는 LLM이 산술하면 회귀하므로(검증됨) 서버가 결정적으로 계산해 제공한다.
|
|
687
|
+
// 재직중 경력의 확정 연차 = updated_at(정보 입력·확인 시점) − start_date — today가 아님(아직 재직
|
|
688
|
+
// 중인지 미확인이라 today로 계산하면 과대 산정·명시 연차와 충돌). 종료 경력 = end_date − start_date.
|
|
689
|
+
// data_age_months = 재직중 정보가 얼마나 오래됐나(크면 '현재도 재직 중인지 확인 필요').
|
|
690
|
+
const ym = (s: string | null | undefined): number | null => {
|
|
691
|
+
const m = s ? /^(\d{4})-(\d{2})/.exec(s) : null;
|
|
692
|
+
return m ? Number(m[1]) * 12 + Number(m[2]) : null;
|
|
693
|
+
};
|
|
694
|
+
const monthsBetween = (a: string | null | undefined, b: string | null | undefined): number | null => {
|
|
695
|
+
const x = ym(a), y = ym(b);
|
|
696
|
+
return x != null && y != null ? y - x : null;
|
|
697
|
+
};
|
|
698
|
+
const tenure = ctx.experiences
|
|
699
|
+
.map((e) => {
|
|
700
|
+
if (!e.start_date) return null;
|
|
701
|
+
const asOf = e.is_current ? (e.updated_at ?? today) : e.end_date;
|
|
702
|
+
const months = monthsBetween(e.start_date, asOf);
|
|
703
|
+
if (months == null || months < 0) return null;
|
|
704
|
+
return {
|
|
705
|
+
company: e.company,
|
|
706
|
+
is_current: e.is_current,
|
|
707
|
+
start_date: e.start_date,
|
|
708
|
+
as_of: (asOf ?? '').slice(0, 7), // 재직중=updated_at(정보 확인 시점) / 종료=end_date
|
|
709
|
+
months,
|
|
710
|
+
years: Math.round((months / 12) * 10) / 10,
|
|
711
|
+
// 음수 방지(미래 updated_at·시계 오차) — 0 미만은 0으로 클램프
|
|
712
|
+
data_age_months: e.is_current ? Math.max(0, monthsBetween(e.updated_at, today) ?? 0) : null,
|
|
713
|
+
};
|
|
714
|
+
})
|
|
715
|
+
.filter((t): t is NonNullable<typeof t> => t != null);
|
|
589
716
|
const enriched = {
|
|
590
717
|
...ctx,
|
|
718
|
+
today,
|
|
719
|
+
tenure,
|
|
591
720
|
recommended_route,
|
|
592
721
|
verifier_sequence: route?.verifierSequence ?? [],
|
|
593
722
|
next_tool: recommended_route ? 'get_workflow_guide' : null,
|
|
594
723
|
};
|
|
595
724
|
const summary = [
|
|
725
|
+
`오늘 날짜: ${today}. 경력 연차·재직기간은 직접 계산하지 말고 제공된 tenure[](서버 계산: 재직중=정보확인일 기준 연차, data_age_months 크면 현재 재직 확인 필요)를 그대로 쓰세요.`,
|
|
596
726
|
`프로필: ${ctx.profile?.name ?? '미입력'}`,
|
|
597
727
|
`이력서 ${ctx.resumes.length} · 경력 ${ctx.experiences.length} · 프로젝트 ${ctx.projects.length} · 스킬 ${ctx.skills.length} · 자소서 ${ctx.cover_letters.length}`,
|
|
598
728
|
ctx.job ? `대상 공고: ${ctx.job.company} · ${ctx.job.position}` : '대상 공고 없음',
|
|
@@ -614,7 +744,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
614
744
|
try {
|
|
615
745
|
const fit = saveFitAnalysis(args);
|
|
616
746
|
const suggested_next_action = {
|
|
617
|
-
prompt: '이 공고에 맞춘 자기소개서를 써드릴까요?',
|
|
747
|
+
prompt: '이 공고에 맞춘 자기소개서를 써드릴까요? (회사 지정 문항·양식·글자수가 있으면 함께 알려주시면 그에 맞춰 작성합니다)',
|
|
618
748
|
options: [
|
|
619
749
|
{
|
|
620
750
|
label: '네, 이 공고에 맞춘 자기소개서를 써줘',
|
|
@@ -634,6 +764,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
634
764
|
'사용자에게 다음 선택지를 보여주세요:',
|
|
635
765
|
'1. 네, 이 공고에 맞춘 자기소개서를 써줘',
|
|
636
766
|
'2. 아니요, 분석만 저장해줘',
|
|
767
|
+
'(1번이면, 회사 지정 문항·양식·글자수가 있으면 알려달라고 요청하세요 — 한국 자소서는 문항형이 많습니다. 없으면 일반 양식으로 진행합니다.)',
|
|
637
768
|
].join('\n'),
|
|
638
769
|
{
|
|
639
770
|
...fit,
|
|
@@ -656,10 +787,11 @@ export const TOOLS: ToolDef[] = [
|
|
|
656
787
|
job_id: z.string(),
|
|
657
788
|
status: z.enum(APPLICATION_STATUSES),
|
|
658
789
|
note: z.string().optional(),
|
|
790
|
+
submission: ApplicationSubmissionSchema.optional(),
|
|
659
791
|
},
|
|
660
792
|
handler: (args) => {
|
|
661
793
|
try {
|
|
662
|
-
const res = updateApplicationStatus(args.job_id, args.status, args.note);
|
|
794
|
+
const res = updateApplicationStatus(args.job_id, args.status, args.note, args.submission);
|
|
663
795
|
const user_message =
|
|
664
796
|
`지원 상태를 '${APPLICATION_STATUS_LABELS[args.status as keyof typeof APPLICATION_STATUS_LABELS]}'(으)로 바꿨어요.` +
|
|
665
797
|
(res.hint ? `\n\n${res.hint}` : '');
|
|
@@ -687,12 +819,19 @@ export const TOOLS: ToolDef[] = [
|
|
|
687
819
|
name: 'save_interview_prep',
|
|
688
820
|
title: '면접 준비 자료 저장',
|
|
689
821
|
description:
|
|
690
|
-
'예상 면접 질문, 꼬리 질문, STAR 답변 가이드, 1분 자기소개 초안 등 면접 준비 자료를 저장합니다.
|
|
822
|
+
'예상 면접 질문, 꼬리 질문, STAR 답변 가이드, 1분 자기소개 초안 등 해당 공고 기준 면접 준비 자료를 저장합니다. 공고/직무/자기소개서를 근거로 당신(AI)이 생성한 자료를 저장하는 데 사용합니다. ' +
|
|
823
|
+
"질문은 두 갈래로 준비하세요 — 각 질문의 category를 'technical'(기술·직무 1차 면접) 또는 'behavioral'(인성·컬처핏·임원 2차 면접, 자기소개서·프로필 기반)로 지정합니다. 가능하면 두 갈래 모두 채워 한 번에 저장하세요(대시보드가 기술/인성·컬처핏 탭으로 나눠 보여줍니다). " +
|
|
824
|
+
'job_id는 필수입니다. 같은 공고에 다시 저장하면 갱신됩니다.',
|
|
691
825
|
inputSchema: InterviewPrepInputSchema.shape,
|
|
692
826
|
handler: (args) => {
|
|
693
827
|
try {
|
|
694
828
|
const prep = saveInterviewPrep(args);
|
|
695
|
-
const
|
|
829
|
+
const tech = prep.questions.filter((q) => (q.category ?? 'technical') === 'technical').length;
|
|
830
|
+
const behavioral = prep.questions.filter((q) => q.category === 'behavioral').length;
|
|
831
|
+
const user_message =
|
|
832
|
+
`면접 준비 자료를 저장했어요. 예상 질문 ${prep.questions.length}개` +
|
|
833
|
+
(behavioral > 0 ? ` (기술 ${tech}개 · 인성·컬처핏 ${behavioral}개)` : '') +
|
|
834
|
+
'가 준비되어 있습니다.';
|
|
696
835
|
return ok(user_message, { ...prep, user_message });
|
|
697
836
|
} catch (e) {
|
|
698
837
|
return fail(e instanceof Error ? e.message : '저장 실패');
|
|
@@ -731,6 +870,68 @@ export const TOOLS: ToolDef[] = [
|
|
|
731
870
|
});
|
|
732
871
|
},
|
|
733
872
|
},
|
|
873
|
+
{
|
|
874
|
+
name: 'export_resume',
|
|
875
|
+
title: '이력서/경력기술서 내보내기',
|
|
876
|
+
description:
|
|
877
|
+
'저장된 이력서·경력기술서 등 문서를 Markdown 또는 인쇄용 HTML(브라우저에서 PDF로 저장) 파일로 내보내 데이터 폴더의 exports에 저장하고, 파일 경로와 본문을 돌려줍니다. 사용자가 공고에 맞춰 정리한 이력서를 먼저 add_resume로 저장한 뒤, "이력서 파일로 받고 싶어"라고 할 때 사용하세요. document_id는 get_resumes로 확인할 수 있습니다. 대시보드에서 직접 다운로드할 수도 있습니다.',
|
|
878
|
+
inputSchema: {
|
|
879
|
+
document_id: z.string(),
|
|
880
|
+
format: z.enum(['md', 'html']).optional().describe('기본 md'),
|
|
881
|
+
},
|
|
882
|
+
handler: (args) => {
|
|
883
|
+
const doc = documentRepo.get(args.document_id);
|
|
884
|
+
if (!doc) return fail('문서를 찾을 수 없습니다.');
|
|
885
|
+
const profile = profileRepo.get();
|
|
886
|
+
const format = args.format ?? 'md';
|
|
887
|
+
const result = format === 'html' ? resumeToHtml(doc, profile) : resumeToMarkdown(doc, profile);
|
|
888
|
+
const dir = getExportsDir();
|
|
889
|
+
const filePath = path.join(dir, result.filename);
|
|
890
|
+
try {
|
|
891
|
+
fs.writeFileSync(filePath, result.content, 'utf8');
|
|
892
|
+
} catch (e) {
|
|
893
|
+
return fail(`파일 저장 실패: ${e instanceof Error ? e.message : e}`);
|
|
894
|
+
}
|
|
895
|
+
return ok(`'${doc.title}'를 ${format.toUpperCase()}로 내보냈습니다.\n저장 위치: ${filePath}`, {
|
|
896
|
+
path: filePath,
|
|
897
|
+
filename: result.filename,
|
|
898
|
+
content: result.content,
|
|
899
|
+
});
|
|
900
|
+
},
|
|
901
|
+
},
|
|
902
|
+
{
|
|
903
|
+
name: 'export_profile',
|
|
904
|
+
title: '프로필을 이력서로 내보내기',
|
|
905
|
+
description:
|
|
906
|
+
'저장된 프로필·경력·프로젝트·보유 기술을 한 장의 이력서 형식으로 모아 Markdown 또는 인쇄용 HTML(브라우저에서 PDF로 저장) 파일로 내보내 데이터 폴더의 exports에 저장하고, 파일 경로와 본문을 돌려줍니다. 사용자가 "내 프로필을 이력서로 뽑아줘"처럼 구조화된 데이터 전체를 이력서로 받고 싶을 때 사용하세요. 특정 공고 맞춤 이력서는 add_resume로 정리·저장한 뒤 export_resume를 쓰세요. 대시보드에서 직접 다운로드할 수도 있습니다.',
|
|
907
|
+
inputSchema: {
|
|
908
|
+
format: z.enum(['md', 'html']).optional().describe('기본 md'),
|
|
909
|
+
},
|
|
910
|
+
handler: (args) => {
|
|
911
|
+
const profile = profileRepo.get();
|
|
912
|
+
if (!profile) return fail('프로필이 없습니다. 먼저 프로필을 저장하세요.');
|
|
913
|
+
const format = args.format ?? 'md';
|
|
914
|
+
const experiences = experienceRepo.list();
|
|
915
|
+
const projects = projectRepo.list();
|
|
916
|
+
const skills = skillRepo.list();
|
|
917
|
+
const result =
|
|
918
|
+
format === 'html'
|
|
919
|
+
? profileToHtml(profile, experiences, projects, skills)
|
|
920
|
+
: profileToMarkdown(profile, experiences, projects, skills);
|
|
921
|
+
const dir = getExportsDir();
|
|
922
|
+
const filePath = path.join(dir, result.filename);
|
|
923
|
+
try {
|
|
924
|
+
fs.writeFileSync(filePath, result.content, 'utf8');
|
|
925
|
+
} catch (e) {
|
|
926
|
+
return fail(`파일 저장 실패: ${e instanceof Error ? e.message : e}`);
|
|
927
|
+
}
|
|
928
|
+
return ok(`프로필을 이력서(${format.toUpperCase()})로 내보냈습니다.\n저장 위치: ${filePath}`, {
|
|
929
|
+
path: filePath,
|
|
930
|
+
filename: result.filename,
|
|
931
|
+
content: result.content,
|
|
932
|
+
});
|
|
933
|
+
},
|
|
934
|
+
},
|
|
734
935
|
|
|
735
936
|
/* ----------------------------------------------------------- dashboard ui */
|
|
736
937
|
{
|
|
@@ -780,10 +981,18 @@ export const TOOLS: ToolDef[] = [
|
|
|
780
981
|
name: 'get_workflow_guide',
|
|
781
982
|
title: '워크플로우 가이드',
|
|
782
983
|
description:
|
|
783
|
-
'커리어 작업(공고
|
|
984
|
+
'커리어 작업(공고 분석·자소서·경력기술서·면접 준비 등)을 시작하기 직전에 호출하세요. 표준 워크플로우의 단계별 안내에 더해, 그 작업의 "전문가 실행 절차(EOP)"와 적용할 전문가 플레이북·저장 전 검증기 순서를 한 번에 돌려줍니다. 안내된 대로 get_playbook으로 도메인 지식을, 저장 직전 get_verifier로 검증 루브릭을 받아 적용하세요. workflow_id를 생략하면 전체 목록을 돌려줍니다.',
|
|
784
985
|
inputSchema: {
|
|
785
986
|
workflow_id: z
|
|
786
|
-
.enum([
|
|
987
|
+
.enum([
|
|
988
|
+
'onboarding',
|
|
989
|
+
'analyze_job',
|
|
990
|
+
'write_cover_letter',
|
|
991
|
+
'write_career_description',
|
|
992
|
+
'manage_application_status',
|
|
993
|
+
'prepare_interview',
|
|
994
|
+
'build_personal_brand',
|
|
995
|
+
])
|
|
787
996
|
.optional(),
|
|
788
997
|
},
|
|
789
998
|
readOnly: true,
|
|
@@ -899,10 +1108,27 @@ export const TOOLS: ToolDef[] = [
|
|
|
899
1108
|
name: 'get_writing_style_guide',
|
|
900
1109
|
title: 'AI 티 안 나는 글쓰기 가이드',
|
|
901
1110
|
description:
|
|
902
|
-
'자기소개서·자기 PR·지원 메일 등 사람이 쓴 듯한 글을 작성하기 직전에 호출하세요. "AI가 쓴 티"가 나는 한국어 문장 습관(번역투, 클리셰, 기계적 병렬, 상투적 연결어, 균일한 문장 리듬 등)을 제거하는 작성 규칙과 저장 전 자가 점검 항목을 돌려줍니다. 사실·수치·고유명사는 그대로 두고 문장 결만 다듬도록 안내합니다. write_cover_letter / analyze_job 워크플로우에서
|
|
903
|
-
inputSchema: {
|
|
1111
|
+
'자기소개서·자기 PR·지원 메일 등 사람이 쓴 듯한 글을 작성하기 직전에 호출하세요. "AI가 쓴 티"가 나는 한국어 문장 습관(번역투, 클리셰, 기계적 병렬, 상투적 연결어, 균일한 문장 리듬 등)을 제거하는 작성 규칙과 저장 전 자가 점검 항목을 돌려줍니다. 사실·수치·고유명사는 그대로 두고 문장 결만 다듬도록 안내합니다. 경력기술서·이력서·포트폴리오를 쓸 때는 doc_type을 함께 주면 불릿·번호 성과 같은 구조를 산문으로 평탄화하지 않도록 구조 보존 보정 지침을 더해 돌려줍니다. write_cover_letter / write_career_description / analyze_job 워크플로우에서 글을 쓸 때 반드시 함께 적용하세요.',
|
|
1112
|
+
inputSchema: {
|
|
1113
|
+
doc_type: z
|
|
1114
|
+
.enum([
|
|
1115
|
+
'cover_letter',
|
|
1116
|
+
'self_pr',
|
|
1117
|
+
'application_email',
|
|
1118
|
+
'career_description',
|
|
1119
|
+
'resume',
|
|
1120
|
+
'portfolio',
|
|
1121
|
+
])
|
|
1122
|
+
.optional()
|
|
1123
|
+
.describe(
|
|
1124
|
+
'작성할 문서 유형. career_description·resume·portfolio는 불릿·구조를 보존하는 보정을 더해 돌려줍니다. 생략하면 자기소개서 등 산문형 기준.',
|
|
1125
|
+
),
|
|
1126
|
+
},
|
|
904
1127
|
readOnly: true,
|
|
905
|
-
handler: () =>
|
|
1128
|
+
handler: (args) =>
|
|
1129
|
+
ok('AI 티 안 나는 글쓰기 가이드를 적용해 작성하세요.', {
|
|
1130
|
+
guide: getWritingStyleGuide(args?.doc_type),
|
|
1131
|
+
}),
|
|
906
1132
|
},
|
|
907
1133
|
|
|
908
1134
|
/* ----------------------------------------------------------------- update */
|
|
@@ -914,7 +1140,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
914
1140
|
inputSchema: {},
|
|
915
1141
|
readOnly: true,
|
|
916
1142
|
handler: async () => {
|
|
917
|
-
const u = await getUpdateStatusAsync();
|
|
1143
|
+
const u = await getUpdateStatusAsync(true); // "지금 확인" — 신선한 캐시 무시하고 재조회
|
|
918
1144
|
if (u.latest === null) return ok('업데이트 확인에 실패했습니다(오프라인이거나 일시적 오류). 잠시 후 다시 시도하세요.', u);
|
|
919
1145
|
const user_message = u.update_available
|
|
920
1146
|
? `새 버전 v${u.latest}이 나왔어요. 현재 버전은 v${u.current}입니다. 원하시면 지금 업데이트할 수 있습니다.`
|
|
@@ -193,17 +193,34 @@ export function getUpdateStatus(): UpdateStatus {
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
/**
|
|
196
|
-
* 즉시(바운드) 업데이트 확인.
|
|
197
|
-
*
|
|
196
|
+
* 즉시(바운드) 업데이트 확인. doctor·check_for_update처럼 "지금 확인" 의미가 분명한
|
|
197
|
+
* 일회성 호출에서 사용.
|
|
198
|
+
*
|
|
199
|
+
* `force=true`("지금 확인")면 신선한 24h 캐시여도 무시하고 레지스트리를 새로 읽는다 —
|
|
200
|
+
* 캐시가 직전 릴리스(예: 0.0.3) 시점에 채워진 채 새 버전(0.0.4)이 나오면, 설치본보다
|
|
201
|
+
* 오래된 `latest`를 돌려주던 버그를 막는다. 강제 조회가 네트워크 오류로 실패하면 멀쩡한
|
|
202
|
+
* 기존 캐시를 null로 덮지 않고(비차단 신호 getUpdateStatus 보호) "확인 실패"로만 보고한다.
|
|
203
|
+
*
|
|
204
|
+
* `force=false`(기본, 만료 재확인)면 기존대로 캐시를 존중하고, 실패는 짧은 TTL의 null로
|
|
205
|
+
* 기록해 잦은 재시도를 막는다.
|
|
198
206
|
*/
|
|
199
|
-
export async function getUpdateStatusAsync(): Promise<UpdateStatus> {
|
|
207
|
+
export async function getUpdateStatusAsync(force = false): Promise<UpdateStatus> {
|
|
200
208
|
if (checkDisabled()) return buildStatus(null);
|
|
201
|
-
|
|
202
|
-
if (cacheExpired(cache))
|
|
203
|
-
|
|
204
|
-
|
|
209
|
+
const cache = readCache();
|
|
210
|
+
if (!force && !cacheExpired(cache)) return buildStatus(cache);
|
|
211
|
+
|
|
212
|
+
const latest = await fetchLatest();
|
|
213
|
+
if (latest !== null) {
|
|
214
|
+
const fresh = { checkedAt: Date.now(), latest };
|
|
215
|
+
writeCache(fresh);
|
|
216
|
+
return buildStatus(fresh);
|
|
205
217
|
}
|
|
206
|
-
|
|
218
|
+
// fetch 실패. 강제("지금 확인")는 정직하게 실패로 보고하되 멀쩡한 기존 캐시는 보존한다.
|
|
219
|
+
if (force) return buildStatus(null);
|
|
220
|
+
// 비강제(만료 재확인)는 기존처럼 null을 짧은 TTL로 기록해 잦은 재시도를 막는다.
|
|
221
|
+
const failed = { checkedAt: Date.now(), latest: null };
|
|
222
|
+
writeCache(failed);
|
|
223
|
+
return buildStatus(failed);
|
|
207
224
|
}
|
|
208
225
|
|
|
209
226
|
export interface UpdateResult {
|
|
@@ -288,13 +305,11 @@ export async function runSelfUpdate(): Promise<UpdateResult> {
|
|
|
288
305
|
manual: manualCommand(),
|
|
289
306
|
};
|
|
290
307
|
}
|
|
291
|
-
// 최신 버전:
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
const latest = cache?.latest ?? null;
|
|
308
|
+
// 최신 버전: "업데이트해줘"는 명시적 사용자 행동이라 신선한 캐시여도 새로 조회한다 —
|
|
309
|
+
// 직전 릴리스 시점에 채워진 캐시가 'already latest'로 오판해 실제 업데이트를 건너뛰지 않게.
|
|
310
|
+
// 실패(null)면 아래 가드가 안내로 빠지며, 멀쩡한 기존 캐시는 덮지 않는다.
|
|
311
|
+
const latest = await fetchLatest();
|
|
312
|
+
if (latest !== null) writeCache({ checkedAt: Date.now(), latest });
|
|
298
313
|
// 최신 버전을 확인하지 못함(오프라인/레지스트리 오류): 무엇을 설치할지 모른 채 실행 중인
|
|
299
314
|
// 설치를 덮어쓰면 반파 위험이 있으므로 시도하지 않고 안내한다.
|
|
300
315
|
if (latest === null) {
|