careermate 0.0.4 → 0.0.6
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 +11 -7
- package/INSTALL.md +18 -16
- package/README.md +28 -26
- package/THIRD_PARTY_NOTICES.md +2 -3
- package/bin/careermate.mjs +8 -1
- package/dist/career-os/README.md +8 -3
- package/dist/career-os/eop/career-description.md +136 -0
- package/dist/career-os/eop/cover-letter.md +7 -5
- package/dist/career-os/eop/fit-analysis.md +15 -5
- package/dist/career-os/eop/interview-prep.md +209 -21
- package/dist/career-os/eop/job-analysis.md +5 -3
- package/dist/career-os/eop/profile-extraction.md +5 -4
- package/dist/career-os/knowledge/cover-letter.md +4 -4
- package/dist/career-os/knowledge/fit-matching.md +8 -8
- 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 +2 -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 +1019 -127
- package/dist/mcp.mjs +1036 -126
- 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 +25 -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 +270 -72
- package/dist/public/styles.css +38 -3
- package/dist/web.mjs +859 -103
- package/docs/FAQ.md +26 -5
- package/docs/MCP_TOOLS.md +26 -12
- package/docs/START_WORKFLOW.md +2 -2
- package/docs/SUPPORTED_AI_APPS.md +13 -0
- package/docs/WORKFLOWS.md +80 -23
- package/docs/career-os/README.md +8 -3
- package/docs/career-os/eop/career-description.md +136 -0
- package/docs/career-os/eop/cover-letter.md +7 -5
- package/docs/career-os/eop/fit-analysis.md +15 -5
- package/docs/career-os/eop/interview-prep.md +209 -21
- package/docs/career-os/eop/job-analysis.md +5 -3
- package/docs/career-os/eop/profile-extraction.md +5 -4
- package/docs/career-os/knowledge/cover-letter.md +4 -4
- package/docs/career-os/knowledge/fit-matching.md +8 -8
- 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 +2 -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 +138 -21
- 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 +46 -7
- package/packages/mcp-tools/src/bridge.ts +194 -13
- package/packages/mcp-tools/src/tools.ts +390 -38
- package/packages/mcp-tools/src/update.ts +31 -16
- 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 +17 -8
- package/packages/shared/src/enums.ts +14 -4
- package/packages/shared/src/schemas.ts +41 -0
- package/packages/workflows/src/definitions.ts +47 -10
- 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 -565
- package/dist/site/landing.js +0 -54
- 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 -353
- package/dist/site/style.css +0 -559
- package/dist/site/vercel.json +0 -20
|
@@ -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,13 +76,22 @@ 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';
|
|
82
90
|
import { getUpdateStatus, getUpdateStatusAsync, runSelfUpdate } from './update.ts';
|
|
83
91
|
|
|
84
92
|
const STATUS_LIST = APPLICATION_STATUSES.map((s) => `${s}(${APPLICATION_STATUS_LABELS[s]})`).join(', ');
|
|
93
|
+
const DELETE_CONFIRMATION = 'DELETE';
|
|
94
|
+
const DASHBOARD_URL = 'http://127.0.0.1:4319';
|
|
85
95
|
|
|
86
96
|
/** List real, user-dropped files in the inbox (skips hidden/OS/office-lock cruft). */
|
|
87
97
|
function listInboxFiles(dir: string): { name: string; path: string }[] {
|
|
@@ -91,15 +101,28 @@ function listInboxFiles(dir: string): { name: string; path: string }[] {
|
|
|
91
101
|
} catch {
|
|
92
102
|
return [];
|
|
93
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
|
+
}
|
|
94
113
|
const SKIP = /^[.~]|^(thumbs\.db|desktop\.ini)$/i;
|
|
95
114
|
const out: { name: string; path: string }[] = [];
|
|
96
115
|
for (const name of names) {
|
|
97
116
|
if (SKIP.test(name)) continue;
|
|
98
117
|
const p = path.join(dir, name);
|
|
99
118
|
try {
|
|
100
|
-
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 });
|
|
101
124
|
} catch {
|
|
102
|
-
/* unreadable entry — skip */
|
|
125
|
+
/* unreadable entry / broken link — skip */
|
|
103
126
|
}
|
|
104
127
|
}
|
|
105
128
|
return out;
|
|
@@ -123,13 +146,15 @@ async function mapWithLimit<T, R>(items: T[], limit: number, fn: (item: T, index
|
|
|
123
146
|
return out;
|
|
124
147
|
}
|
|
125
148
|
|
|
126
|
-
/** Statuses at/after which interview prep is
|
|
149
|
+
/** Statuses at/after which interview prep is recommended as a next step. */
|
|
127
150
|
const INTERVIEW_UNLOCKED = ['document_passed', 'interview', 'final_passed'];
|
|
128
151
|
|
|
129
152
|
/**
|
|
130
153
|
* Pick the recommended Career-OS route from the loaded context, so the AI is
|
|
131
154
|
* nudged onto the expert procedure for what it's most likely about to do.
|
|
132
|
-
* 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.
|
|
133
158
|
*/
|
|
134
159
|
function pickRoute(ctx: ReturnType<typeof getApplicationContext>): string | null {
|
|
135
160
|
if (!ctx.job) return null;
|
|
@@ -188,9 +213,9 @@ export const TOOLS: ToolDef[] = [
|
|
|
188
213
|
/* --------------------------------------------------------- inbox (파일 인입) */
|
|
189
214
|
{
|
|
190
215
|
name: 'open_inbox',
|
|
191
|
-
title: '
|
|
216
|
+
title: '문서 인입 폴더 열기',
|
|
192
217
|
description:
|
|
193
|
-
'CLI 클라이언트는 파일 첨부가 안 되므로, 사용자가 기존
|
|
218
|
+
'CLI 클라이언트는 파일 첨부가 안 되므로, 사용자가 기존 이력서·경력기술서·포트폴리오·자기소개서 파일(hwp/hwpx/docx/pdf 등)을 끌어다 넣을 수 있는 로컬 인입 폴더(~/.careermate/inbox)를 보장하고 파일 탐색기로 엽니다. 온보딩에서 "기존에 작성해 둔 문서 파일이 있다"고 사용자가 답했을 때만 호출하세요 — 묻지도 않고 폴더를 자동으로 열지 마세요. 파일 경로를 직접 알려주는 사용자에게는 이 도구 대신 read_document를 쓰세요. 호출하면 폴더 경로와 현재 폴더에 들어 있는 파일 목록을 돌려줍니다. 사용자가 파일을 다 넣었다고 하면 read_inbox로 본문을 읽으세요.',
|
|
194
219
|
inputSchema: {},
|
|
195
220
|
handler: () => {
|
|
196
221
|
const dir = getInboxDir();
|
|
@@ -198,7 +223,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
198
223
|
const files = listInboxFiles(dir);
|
|
199
224
|
const list = files.length ? files.map((f) => `- ${f.name}`).join('\n') : '(아직 비어 있음)';
|
|
200
225
|
return ok(
|
|
201
|
-
|
|
226
|
+
`문서 인입 폴더를 열었습니다:\n${dir}\n\n여기에 이력서·경력기술서·포트폴리오·기존 자기소개서 파일을 넣고 "다 넣었어"라고 알려주세요. (hwp/hwpx/docx는 자동으로 텍스트를 추출하고, pdf·이미지는 제가 직접 읽습니다.)\n\n현재 파일:\n${list}`,
|
|
202
227
|
{ dir, files },
|
|
203
228
|
);
|
|
204
229
|
},
|
|
@@ -207,7 +232,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
207
232
|
name: 'read_inbox',
|
|
208
233
|
title: '인입 폴더 파일 읽기',
|
|
209
234
|
description:
|
|
210
|
-
'인입 폴더(~/.careermate/inbox)에 사용자가 넣어 둔 문서들의 본문을 한 번에 읽어 돌려줍니다. open_inbox로 폴더를 안내한 뒤 사용자가 "다 넣었어"라고 하면 호출하세요. docx/hwp/hwpx/텍스트는 텍스트를 추출하고, pdf·이미지는 추출하지 않고 파일 경로를 돌려주니 AI 클라이언트의 파일 읽기 기능으로 그 경로를 직접 열어 읽으세요. 읽은 내용을 구조화해 save_profile
|
|
235
|
+
'인입 폴더(~/.careermate/inbox)에 사용자가 넣어 둔 문서들의 본문을 한 번에 읽어 돌려줍니다. open_inbox로 폴더를 안내한 뒤 사용자가 "다 넣었어"라고 하면 호출하세요. docx/hwp/hwpx/텍스트는 텍스트를 추출하고, pdf·이미지는 추출하지 않고 파일 경로를 돌려주니 AI 클라이언트의 파일 읽기 기능으로 그 경로를 직접 열어 읽으세요. 읽은 내용을 구조화해 프로필은 save_profile, 이력서·경력기술서·포트폴리오는 add_resume, 기존 자기소개서는 save_cover_letter_version으로 저장하세요(파일 내용을 새 파일로 다시 쓰지 마세요). filename을 주면 해당 파일만 읽습니다.',
|
|
211
236
|
inputSchema: {
|
|
212
237
|
filename: z.string().optional().describe('특정 파일만 읽을 때 파일명(대소문자 무시). 생략하면 폴더 안 모든 파일을 읽습니다.'),
|
|
213
238
|
max_chars: z.number().optional().describe('파일별 반환 텍스트 최대 길이(초과 시 잘라냄). 기본 20000자'),
|
|
@@ -250,7 +275,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
250
275
|
)
|
|
251
276
|
.join('\n');
|
|
252
277
|
return ok(
|
|
253
|
-
`인입 폴더 파일 ${files.length}건을 읽었습니다:\n${summary}\n\n추출된 내용을 구조화해 save_profile
|
|
278
|
+
`인입 폴더 파일 ${files.length}건을 읽었습니다:\n${summary}\n\n추출된 내용을 구조화해 프로필은 save_profile, 이력서·경력기술서·포트폴리오는 add_resume, 기존 자기소개서는 save_cover_letter_version으로 저장하세요.`,
|
|
254
279
|
{ dir, files },
|
|
255
280
|
);
|
|
256
281
|
},
|
|
@@ -278,9 +303,19 @@ export const TOOLS: ToolDef[] = [
|
|
|
278
303
|
`더 깐깐하게 보길 원하면 "엄격하게 봐줘"라고 하거나 대시보드 설정에서 엄격 모드를 켤 수 있어요(이력서 본문에 없는 수치까지 차단).`;
|
|
279
304
|
}
|
|
280
305
|
if (upd.update_available) {
|
|
281
|
-
text += `\n\n
|
|
306
|
+
text += `\n\n새 버전 v${upd.latest}이 나왔습니다(현재 v${upd.current}). 사용자가 원하면 바로 업데이트를 진행하세요. 업데이트 후에는 AI 앱 또는 MCP 연결을 재시작해야 적용됩니다.`;
|
|
282
307
|
}
|
|
283
|
-
return ok(text, {
|
|
308
|
+
return ok(text, {
|
|
309
|
+
...s,
|
|
310
|
+
verify_strict: strict,
|
|
311
|
+
update: upd,
|
|
312
|
+
suggested_next_action: upd.update_available
|
|
313
|
+
? {
|
|
314
|
+
prompt: `새 버전 v${upd.latest}이 나왔습니다. 지금 업데이트할까요?`,
|
|
315
|
+
next_tool: 'update_careermate',
|
|
316
|
+
}
|
|
317
|
+
: null,
|
|
318
|
+
});
|
|
284
319
|
},
|
|
285
320
|
},
|
|
286
321
|
|
|
@@ -313,7 +348,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
313
348
|
name: 'add_resume',
|
|
314
349
|
title: '이력서/경력기술서 추가',
|
|
315
350
|
description:
|
|
316
|
-
'이력서, 경력기술서, 포트폴리오 등 문서를 저장합니다. content에는 사용자가 업로드/붙여넣은 텍스트
|
|
351
|
+
'이력서, 경력기술서, 포트폴리오 등 문서를 저장합니다. content에는 사용자가 업로드/붙여넣은 텍스트 또는 AI가 전문가 절차로 정리한 Markdown을 넣으세요. 경력기술서를 작성해 저장할 때는 kind=career_description, source=ai를 사용합니다. kind 기본값은 resume입니다. is_primary=true로 대표 문서를 지정하면 get_application_context의 primary_resume로 노출됩니다.',
|
|
317
352
|
// Reuse the shared (length-bounded, whitespace-rejecting) Document schema so
|
|
318
353
|
// the MCP ingestion path — the primary route for résumé text — inherits the
|
|
319
354
|
// same caps as the web API. kind stays optional here (defaults to resume).
|
|
@@ -337,6 +372,24 @@ export const TOOLS: ToolDef[] = [
|
|
|
337
372
|
return ok(`문서 ${docs.length}건`, docs);
|
|
338
373
|
},
|
|
339
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
|
+
},
|
|
340
393
|
|
|
341
394
|
/* ----------------------------------------------------------- experiences */
|
|
342
395
|
{
|
|
@@ -412,6 +465,60 @@ export const TOOLS: ToolDef[] = [
|
|
|
412
465
|
return ok(`기술 ${list.length}건`, list);
|
|
413
466
|
},
|
|
414
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
|
+
},
|
|
415
522
|
|
|
416
523
|
/* ---------------------------------------------------------- cover letters */
|
|
417
524
|
{
|
|
@@ -451,6 +558,32 @@ export const TOOLS: ToolDef[] = [
|
|
|
451
558
|
}
|
|
452
559
|
},
|
|
453
560
|
},
|
|
561
|
+
{
|
|
562
|
+
name: 'delete_cover_letter',
|
|
563
|
+
title: '자기소개서 삭제',
|
|
564
|
+
description:
|
|
565
|
+
'사용자가 저장된 자기소개서 삭제를 명확히 요청했을 때만 사용합니다. 먼저 get_cover_letters로 전체 목록 또는 공고별 목록을 확인하고, 삭제할 항목을 사용자가 확인한 뒤 호출하세요. 삭제하면 해당 자기소개서의 모든 버전이 함께 삭제되며 되돌릴 수 없습니다. confirm에는 DELETE를 넣으세요.',
|
|
566
|
+
inputSchema: {
|
|
567
|
+
cover_letter_id: z.string(),
|
|
568
|
+
confirm: z.string().describe('삭제 확인값. 정확히 DELETE를 넣어야 삭제됩니다.'),
|
|
569
|
+
},
|
|
570
|
+
handler: (args) => {
|
|
571
|
+
if (args?.confirm !== DELETE_CONFIRMATION) {
|
|
572
|
+
return fail('삭제하려면 confirm에 DELETE를 넣어 다시 호출하세요.');
|
|
573
|
+
}
|
|
574
|
+
const cl = coverLetterRepo.get(args.cover_letter_id, false);
|
|
575
|
+
if (!cl) return fail('자기소개서를 찾을 수 없습니다.');
|
|
576
|
+
const removed = coverLetterRepo.remove(cl.id);
|
|
577
|
+
if (!removed) return fail('자기소개서를 삭제하지 못했습니다.');
|
|
578
|
+
const user_message = `'${cl.title}' 자기소개서를 삭제했어요. 연결된 지원 기록에서는 자기소개서 연결만 비워뒀습니다.`;
|
|
579
|
+
return ok(user_message, {
|
|
580
|
+
deleted: true,
|
|
581
|
+
cover_letter_id: cl.id,
|
|
582
|
+
title: cl.title,
|
|
583
|
+
user_message,
|
|
584
|
+
});
|
|
585
|
+
},
|
|
586
|
+
},
|
|
454
587
|
|
|
455
588
|
/* ------------------------------------------------------------------ jobs */
|
|
456
589
|
{
|
|
@@ -461,18 +594,26 @@ export const TOOLS: ToolDef[] = [
|
|
|
461
594
|
inputSchema: JobInputSchema.shape,
|
|
462
595
|
handler: (args) => {
|
|
463
596
|
const { job } = saveJobPosting(args);
|
|
464
|
-
|
|
597
|
+
const user_message = `'${job.company} · ${job.position}' 공고를 저장했어요.`;
|
|
598
|
+
return ok(user_message, {
|
|
599
|
+
...job,
|
|
600
|
+
user_message,
|
|
601
|
+
suggested_next_action: {
|
|
602
|
+
prompt: '이 공고와 내 프로필을 비교해 적합도를 분석할까요?',
|
|
603
|
+
next_workflow: 'analyze_job',
|
|
604
|
+
},
|
|
605
|
+
});
|
|
465
606
|
},
|
|
466
607
|
},
|
|
467
608
|
{
|
|
468
609
|
name: 'get_job_posting',
|
|
469
610
|
title: '채용공고 상세 조회',
|
|
470
|
-
description: '공고 1건의 상세를 조회합니다. 공고 정보와 함께 적합도 분석, 지원 상태,
|
|
611
|
+
description: '공고 1건의 상세를 조회합니다. 공고 정보와 함께 적합도 분석, 지원 상태, 자기소개서, 면접 준비 자료를 한 번에 돌려줍니다.',
|
|
471
612
|
inputSchema: { job_id: z.string() },
|
|
472
613
|
readOnly: true,
|
|
473
614
|
handler: (args) => {
|
|
474
615
|
const job = jobRepo.get(args.job_id);
|
|
475
|
-
if (!job) return fail(
|
|
616
|
+
if (!job) return fail('공고를 찾을 수 없습니다.');
|
|
476
617
|
return ok(`${job.company} · ${job.position}`, {
|
|
477
618
|
job: jobWithMeta(job),
|
|
478
619
|
fit: fitRepo.getByJob(job.id),
|
|
@@ -493,6 +634,38 @@ export const TOOLS: ToolDef[] = [
|
|
|
493
634
|
return ok(`공고 ${jobs.length}건`, jobs);
|
|
494
635
|
},
|
|
495
636
|
},
|
|
637
|
+
{
|
|
638
|
+
name: 'delete_job_posting',
|
|
639
|
+
title: '채용공고 삭제',
|
|
640
|
+
description:
|
|
641
|
+
'사용자가 저장된 채용공고 삭제를 명확히 요청했을 때만 사용합니다. 먼저 list_jobs로 전체 공고 목록을 확인하고, 삭제할 공고를 사용자가 확인한 뒤 호출하세요. 삭제하면 연결된 지원 상태·적합도 분석·면접 준비가 함께 삭제됩니다. 연결된 자기소개서는 삭제하지 않고 공고 연결만 해제합니다. confirm에는 DELETE를 넣으세요.',
|
|
642
|
+
inputSchema: {
|
|
643
|
+
job_id: z.string(),
|
|
644
|
+
confirm: z.string().describe('삭제 확인값. 정확히 DELETE를 넣어야 삭제됩니다.'),
|
|
645
|
+
},
|
|
646
|
+
handler: (args) => {
|
|
647
|
+
if (args?.confirm !== DELETE_CONFIRMATION) {
|
|
648
|
+
return fail('삭제하려면 confirm에 DELETE를 넣어 다시 호출하세요.');
|
|
649
|
+
}
|
|
650
|
+
const job = jobRepo.get(args.job_id);
|
|
651
|
+
if (!job) return fail('공고를 찾을 수 없습니다.');
|
|
652
|
+
const detachedCoverLetters = coverLetterRepo.listByJob(job.id).length;
|
|
653
|
+
const removed = jobRepo.remove(job.id);
|
|
654
|
+
if (!removed) return fail('공고를 삭제하지 못했습니다.');
|
|
655
|
+
const detachedNote = detachedCoverLetters
|
|
656
|
+
? ` 연결된 자기소개서 ${detachedCoverLetters}건은 삭제하지 않고 공고 연결만 해제했습니다.`
|
|
657
|
+
: '';
|
|
658
|
+
const user_message = `'${job.company} · ${job.position}' 공고를 삭제했어요. 연결된 지원 상태, 적합도 분석, 면접 준비도 함께 정리했습니다.${detachedNote}`;
|
|
659
|
+
return ok(user_message, {
|
|
660
|
+
deleted: true,
|
|
661
|
+
job_id: job.id,
|
|
662
|
+
company: job.company,
|
|
663
|
+
position: job.position,
|
|
664
|
+
detached_cover_letters: detachedCoverLetters,
|
|
665
|
+
user_message,
|
|
666
|
+
});
|
|
667
|
+
},
|
|
668
|
+
},
|
|
496
669
|
|
|
497
670
|
/* ----------------------------------------------------- application context */
|
|
498
671
|
{
|
|
@@ -508,20 +681,54 @@ export const TOOLS: ToolDef[] = [
|
|
|
508
681
|
// 여기에 추천 경로를 실어 전문가 절차로 유도한다. 스키마 변경 없이 반환 JSON만 확장.
|
|
509
682
|
const recommended_route = pickRoute(ctx);
|
|
510
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
|
+
data_age_months: e.is_current ? monthsBetween(e.updated_at, today) : null,
|
|
712
|
+
};
|
|
713
|
+
})
|
|
714
|
+
.filter((t): t is NonNullable<typeof t> => t != null);
|
|
511
715
|
const enriched = {
|
|
512
716
|
...ctx,
|
|
717
|
+
today,
|
|
718
|
+
tenure,
|
|
513
719
|
recommended_route,
|
|
514
720
|
verifier_sequence: route?.verifierSequence ?? [],
|
|
515
721
|
next_tool: recommended_route ? 'get_workflow_guide' : null,
|
|
516
722
|
};
|
|
517
723
|
const summary = [
|
|
724
|
+
`오늘 날짜: ${today}. 경력 연차·재직기간은 직접 계산하지 말고 제공된 tenure[](서버 계산: 재직중=정보확인일 기준 연차, data_age_months 크면 현재 재직 확인 필요)를 그대로 쓰세요.`,
|
|
518
725
|
`프로필: ${ctx.profile?.name ?? '미입력'}`,
|
|
519
726
|
`이력서 ${ctx.resumes.length} · 경력 ${ctx.experiences.length} · 프로젝트 ${ctx.projects.length} · 스킬 ${ctx.skills.length} · 자소서 ${ctx.cover_letters.length}`,
|
|
520
727
|
ctx.job ? `대상 공고: ${ctx.job.company} · ${ctx.job.position}` : '대상 공고 없음',
|
|
521
728
|
ctx.writing_preferences.preferred_tone ? `선호 문체: ${ctx.writing_preferences.preferred_tone}` : '',
|
|
522
729
|
recommended_route
|
|
523
|
-
?
|
|
524
|
-
: '
|
|
730
|
+
? `내부 안내: 사용자에게는 도구명이나 ID를 말하지 말고, 먼저 get_workflow_guide({ workflow_id: "${recommended_route}" })로 실행 절차와 검증 순서를 받은 뒤 쉬운 말로 안내하세요.`
|
|
731
|
+
: '내부 안내: 사용자에게는 도구명을 말하지 말고, 먼저 get_workflow_guide로 실행 절차와 검증 순서를 받은 뒤 쉬운 말로 안내하세요.',
|
|
525
732
|
].filter(Boolean).join('\n');
|
|
526
733
|
return ok(summary, enriched);
|
|
527
734
|
},
|
|
@@ -535,7 +742,34 @@ export const TOOLS: ToolDef[] = [
|
|
|
535
742
|
handler: (args) => {
|
|
536
743
|
try {
|
|
537
744
|
const fit = saveFitAnalysis(args);
|
|
538
|
-
|
|
745
|
+
const suggested_next_action = {
|
|
746
|
+
prompt: '이 공고에 맞춘 자기소개서를 써드릴까요?',
|
|
747
|
+
options: [
|
|
748
|
+
{
|
|
749
|
+
label: '네, 이 공고에 맞춘 자기소개서를 써줘',
|
|
750
|
+
next_workflow: 'write_cover_letter',
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
label: '아니요, 분석만 저장해줘',
|
|
754
|
+
next_workflow: null,
|
|
755
|
+
},
|
|
756
|
+
],
|
|
757
|
+
};
|
|
758
|
+
const user_message = `적합도 분석을 저장했어요${fit.score != null ? ` (${fit.score}점)` : ''}. 자세한 결과는 대시보드에서도 확인할 수 있습니다: ${DASHBOARD_URL}`;
|
|
759
|
+
return ok(
|
|
760
|
+
[
|
|
761
|
+
user_message,
|
|
762
|
+
'',
|
|
763
|
+
'사용자에게 다음 선택지를 보여주세요:',
|
|
764
|
+
'1. 네, 이 공고에 맞춘 자기소개서를 써줘',
|
|
765
|
+
'2. 아니요, 분석만 저장해줘',
|
|
766
|
+
].join('\n'),
|
|
767
|
+
{
|
|
768
|
+
...fit,
|
|
769
|
+
user_message,
|
|
770
|
+
suggested_next_action,
|
|
771
|
+
},
|
|
772
|
+
);
|
|
539
773
|
} catch (e) {
|
|
540
774
|
return fail(e instanceof Error ? e.message : '저장 실패');
|
|
541
775
|
}
|
|
@@ -551,13 +785,26 @@ export const TOOLS: ToolDef[] = [
|
|
|
551
785
|
job_id: z.string(),
|
|
552
786
|
status: z.enum(APPLICATION_STATUSES),
|
|
553
787
|
note: z.string().optional(),
|
|
788
|
+
submission: ApplicationSubmissionSchema.optional(),
|
|
554
789
|
},
|
|
555
790
|
handler: (args) => {
|
|
556
791
|
try {
|
|
557
|
-
const res = updateApplicationStatus(args.job_id, args.status, args.note);
|
|
792
|
+
const res = updateApplicationStatus(args.job_id, args.status, args.note, args.submission);
|
|
793
|
+
const user_message =
|
|
794
|
+
`지원 상태를 '${APPLICATION_STATUS_LABELS[args.status as keyof typeof APPLICATION_STATUS_LABELS]}'(으)로 바꿨어요.` +
|
|
795
|
+
(res.hint ? `\n\n${res.hint}` : '');
|
|
558
796
|
return ok(
|
|
559
|
-
|
|
560
|
-
|
|
797
|
+
user_message,
|
|
798
|
+
{
|
|
799
|
+
...res,
|
|
800
|
+
user_message,
|
|
801
|
+
suggested_next_action: res.hint
|
|
802
|
+
? {
|
|
803
|
+
prompt: res.hint,
|
|
804
|
+
next_workflow: 'prepare_interview',
|
|
805
|
+
}
|
|
806
|
+
: null,
|
|
807
|
+
},
|
|
561
808
|
);
|
|
562
809
|
} catch (e) {
|
|
563
810
|
return fail(e instanceof Error ? e.message : '변경 실패');
|
|
@@ -570,12 +817,20 @@ export const TOOLS: ToolDef[] = [
|
|
|
570
817
|
name: 'save_interview_prep',
|
|
571
818
|
title: '면접 준비 자료 저장',
|
|
572
819
|
description:
|
|
573
|
-
'예상 면접 질문, 꼬리 질문, STAR 답변 가이드, 1분 자기소개 초안 등 면접 준비 자료를 저장합니다.
|
|
820
|
+
'예상 면접 질문, 꼬리 질문, STAR 답변 가이드, 1분 자기소개 초안 등 해당 공고 기준 면접 준비 자료를 저장합니다. 공고/직무/자기소개서를 근거로 당신(AI)이 생성한 자료를 저장하는 데 사용합니다. ' +
|
|
821
|
+
"질문은 두 갈래로 준비하세요 — 각 질문의 category를 'technical'(기술·직무 1차 면접) 또는 'behavioral'(인성·컬처핏·임원 2차 면접, 자기소개서·프로필 기반)로 지정합니다. 가능하면 두 갈래 모두 채워 한 번에 저장하세요(대시보드가 기술/인성·컬처핏 탭으로 나눠 보여줍니다). " +
|
|
822
|
+
'job_id는 필수입니다. 같은 공고에 다시 저장하면 갱신됩니다.',
|
|
574
823
|
inputSchema: InterviewPrepInputSchema.shape,
|
|
575
824
|
handler: (args) => {
|
|
576
825
|
try {
|
|
577
826
|
const prep = saveInterviewPrep(args);
|
|
578
|
-
|
|
827
|
+
const tech = prep.questions.filter((q) => (q.category ?? 'technical') === 'technical').length;
|
|
828
|
+
const behavioral = prep.questions.filter((q) => q.category === 'behavioral').length;
|
|
829
|
+
const user_message =
|
|
830
|
+
`면접 준비 자료를 저장했어요. 예상 질문 ${prep.questions.length}개` +
|
|
831
|
+
(behavioral > 0 ? ` (기술 ${tech}개 · 인성·컬처핏 ${behavioral}개)` : '') +
|
|
832
|
+
'가 준비되어 있습니다.';
|
|
833
|
+
return ok(user_message, { ...prep, user_message });
|
|
579
834
|
} catch (e) {
|
|
580
835
|
return fail(e instanceof Error ? e.message : '저장 실패');
|
|
581
836
|
}
|
|
@@ -613,6 +868,68 @@ export const TOOLS: ToolDef[] = [
|
|
|
613
868
|
});
|
|
614
869
|
},
|
|
615
870
|
},
|
|
871
|
+
{
|
|
872
|
+
name: 'export_resume',
|
|
873
|
+
title: '이력서/경력기술서 내보내기',
|
|
874
|
+
description:
|
|
875
|
+
'저장된 이력서·경력기술서 등 문서를 Markdown 또는 인쇄용 HTML(브라우저에서 PDF로 저장) 파일로 내보내 데이터 폴더의 exports에 저장하고, 파일 경로와 본문을 돌려줍니다. 사용자가 공고에 맞춰 정리한 이력서를 먼저 add_resume로 저장한 뒤, "이력서 파일로 받고 싶어"라고 할 때 사용하세요. document_id는 get_resumes로 확인할 수 있습니다. 대시보드에서 직접 다운로드할 수도 있습니다.',
|
|
876
|
+
inputSchema: {
|
|
877
|
+
document_id: z.string(),
|
|
878
|
+
format: z.enum(['md', 'html']).optional().describe('기본 md'),
|
|
879
|
+
},
|
|
880
|
+
handler: (args) => {
|
|
881
|
+
const doc = documentRepo.get(args.document_id);
|
|
882
|
+
if (!doc) return fail('문서를 찾을 수 없습니다.');
|
|
883
|
+
const profile = profileRepo.get();
|
|
884
|
+
const format = args.format ?? 'md';
|
|
885
|
+
const result = format === 'html' ? resumeToHtml(doc, profile) : resumeToMarkdown(doc, profile);
|
|
886
|
+
const dir = getExportsDir();
|
|
887
|
+
const filePath = path.join(dir, result.filename);
|
|
888
|
+
try {
|
|
889
|
+
fs.writeFileSync(filePath, result.content, 'utf8');
|
|
890
|
+
} catch (e) {
|
|
891
|
+
return fail(`파일 저장 실패: ${e instanceof Error ? e.message : e}`);
|
|
892
|
+
}
|
|
893
|
+
return ok(`'${doc.title}'를 ${format.toUpperCase()}로 내보냈습니다.\n저장 위치: ${filePath}`, {
|
|
894
|
+
path: filePath,
|
|
895
|
+
filename: result.filename,
|
|
896
|
+
content: result.content,
|
|
897
|
+
});
|
|
898
|
+
},
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
name: 'export_profile',
|
|
902
|
+
title: '프로필을 이력서로 내보내기',
|
|
903
|
+
description:
|
|
904
|
+
'저장된 프로필·경력·프로젝트·보유 기술을 한 장의 이력서 형식으로 모아 Markdown 또는 인쇄용 HTML(브라우저에서 PDF로 저장) 파일로 내보내 데이터 폴더의 exports에 저장하고, 파일 경로와 본문을 돌려줍니다. 사용자가 "내 프로필을 이력서로 뽑아줘"처럼 구조화된 데이터 전체를 이력서로 받고 싶을 때 사용하세요. 특정 공고 맞춤 이력서는 add_resume로 정리·저장한 뒤 export_resume를 쓰세요. 대시보드에서 직접 다운로드할 수도 있습니다.',
|
|
905
|
+
inputSchema: {
|
|
906
|
+
format: z.enum(['md', 'html']).optional().describe('기본 md'),
|
|
907
|
+
},
|
|
908
|
+
handler: (args) => {
|
|
909
|
+
const profile = profileRepo.get();
|
|
910
|
+
if (!profile) return fail('프로필이 없습니다. 먼저 프로필을 저장하세요.');
|
|
911
|
+
const format = args.format ?? 'md';
|
|
912
|
+
const experiences = experienceRepo.list();
|
|
913
|
+
const projects = projectRepo.list();
|
|
914
|
+
const skills = skillRepo.list();
|
|
915
|
+
const result =
|
|
916
|
+
format === 'html'
|
|
917
|
+
? profileToHtml(profile, experiences, projects, skills)
|
|
918
|
+
: profileToMarkdown(profile, experiences, projects, skills);
|
|
919
|
+
const dir = getExportsDir();
|
|
920
|
+
const filePath = path.join(dir, result.filename);
|
|
921
|
+
try {
|
|
922
|
+
fs.writeFileSync(filePath, result.content, 'utf8');
|
|
923
|
+
} catch (e) {
|
|
924
|
+
return fail(`파일 저장 실패: ${e instanceof Error ? e.message : e}`);
|
|
925
|
+
}
|
|
926
|
+
return ok(`프로필을 이력서(${format.toUpperCase()})로 내보냈습니다.\n저장 위치: ${filePath}`, {
|
|
927
|
+
path: filePath,
|
|
928
|
+
filename: result.filename,
|
|
929
|
+
content: result.content,
|
|
930
|
+
});
|
|
931
|
+
},
|
|
932
|
+
},
|
|
616
933
|
|
|
617
934
|
/* ----------------------------------------------------------- dashboard ui */
|
|
618
935
|
{
|
|
@@ -662,10 +979,18 @@ export const TOOLS: ToolDef[] = [
|
|
|
662
979
|
name: 'get_workflow_guide',
|
|
663
980
|
title: '워크플로우 가이드',
|
|
664
981
|
description:
|
|
665
|
-
'커리어 작업(공고
|
|
982
|
+
'커리어 작업(공고 분석·자소서·경력기술서·면접 준비 등)을 시작하기 직전에 호출하세요. 표준 워크플로우의 단계별 안내에 더해, 그 작업의 "전문가 실행 절차(EOP)"와 적용할 전문가 플레이북·저장 전 검증기 순서를 한 번에 돌려줍니다. 안내된 대로 get_playbook으로 도메인 지식을, 저장 직전 get_verifier로 검증 루브릭을 받아 적용하세요. workflow_id를 생략하면 전체 목록을 돌려줍니다.',
|
|
666
983
|
inputSchema: {
|
|
667
984
|
workflow_id: z
|
|
668
|
-
.enum([
|
|
985
|
+
.enum([
|
|
986
|
+
'onboarding',
|
|
987
|
+
'analyze_job',
|
|
988
|
+
'write_cover_letter',
|
|
989
|
+
'write_career_description',
|
|
990
|
+
'manage_application_status',
|
|
991
|
+
'prepare_interview',
|
|
992
|
+
'build_personal_brand',
|
|
993
|
+
])
|
|
669
994
|
.optional(),
|
|
670
995
|
},
|
|
671
996
|
readOnly: true,
|
|
@@ -781,10 +1106,27 @@ export const TOOLS: ToolDef[] = [
|
|
|
781
1106
|
name: 'get_writing_style_guide',
|
|
782
1107
|
title: 'AI 티 안 나는 글쓰기 가이드',
|
|
783
1108
|
description:
|
|
784
|
-
'자기소개서·자기 PR·지원 메일 등 사람이 쓴 듯한 글을 작성하기 직전에 호출하세요. "AI가 쓴 티"가 나는 한국어 문장 습관(번역투, 클리셰, 기계적 병렬, 상투적 연결어, 균일한 문장 리듬 등)을 제거하는 작성 규칙과 저장 전 자가 점검 항목을 돌려줍니다. 사실·수치·고유명사는 그대로 두고 문장 결만 다듬도록 안내합니다. write_cover_letter / analyze_job 워크플로우에서
|
|
785
|
-
inputSchema: {
|
|
1109
|
+
'자기소개서·자기 PR·지원 메일 등 사람이 쓴 듯한 글을 작성하기 직전에 호출하세요. "AI가 쓴 티"가 나는 한국어 문장 습관(번역투, 클리셰, 기계적 병렬, 상투적 연결어, 균일한 문장 리듬 등)을 제거하는 작성 규칙과 저장 전 자가 점검 항목을 돌려줍니다. 사실·수치·고유명사는 그대로 두고 문장 결만 다듬도록 안내합니다. 경력기술서·이력서·포트폴리오를 쓸 때는 doc_type을 함께 주면 불릿·번호 성과 같은 구조를 산문으로 평탄화하지 않도록 구조 보존 보정 지침을 더해 돌려줍니다. write_cover_letter / write_career_description / analyze_job 워크플로우에서 글을 쓸 때 반드시 함께 적용하세요.',
|
|
1110
|
+
inputSchema: {
|
|
1111
|
+
doc_type: z
|
|
1112
|
+
.enum([
|
|
1113
|
+
'cover_letter',
|
|
1114
|
+
'self_pr',
|
|
1115
|
+
'application_email',
|
|
1116
|
+
'career_description',
|
|
1117
|
+
'resume',
|
|
1118
|
+
'portfolio',
|
|
1119
|
+
])
|
|
1120
|
+
.optional()
|
|
1121
|
+
.describe(
|
|
1122
|
+
'작성할 문서 유형. career_description·resume·portfolio는 불릿·구조를 보존하는 보정을 더해 돌려줍니다. 생략하면 자기소개서 등 산문형 기준.',
|
|
1123
|
+
),
|
|
1124
|
+
},
|
|
786
1125
|
readOnly: true,
|
|
787
|
-
handler: () =>
|
|
1126
|
+
handler: (args) =>
|
|
1127
|
+
ok('AI 티 안 나는 글쓰기 가이드를 적용해 작성하세요.', {
|
|
1128
|
+
guide: getWritingStyleGuide(args?.doc_type),
|
|
1129
|
+
}),
|
|
788
1130
|
},
|
|
789
1131
|
|
|
790
1132
|
/* ----------------------------------------------------------------- update */
|
|
@@ -792,17 +1134,27 @@ export const TOOLS: ToolDef[] = [
|
|
|
792
1134
|
name: 'check_for_update',
|
|
793
1135
|
title: '업데이트 확인',
|
|
794
1136
|
description:
|
|
795
|
-
'CareerMate에 새 버전이 있는지 npm 레지스트리에서 지금 확인합니다(공개 버전 정보만 GET하며 사용자 데이터는 전송하지 않습니다). 현재 버전·최신 버전·업데이트 가능 여부를 돌려줍니다. 사용자가 "최신이야?/업데이트 있어?"라고 묻거나, get_onboarding_status가 새 버전을 알릴 때 호출하세요.
|
|
1137
|
+
'CareerMate에 새 버전이 있는지 npm 레지스트리에서 지금 확인합니다(공개 버전 정보만 GET하며 사용자 데이터는 전송하지 않습니다). 현재 버전·최신 버전·업데이트 가능 여부를 돌려줍니다. 사용자가 "최신이야?/업데이트 있어?"라고 묻거나, get_onboarding_status가 새 버전을 알릴 때 호출하세요. 확인만 하고 설치는 하지 않습니다. 사용자가 "업데이트해줘/최신으로 올려줘"처럼 명시적으로 요청하면 update_careermate를 호출하세요.',
|
|
796
1138
|
inputSchema: {},
|
|
797
1139
|
readOnly: true,
|
|
798
1140
|
handler: async () => {
|
|
799
|
-
const u = await getUpdateStatusAsync();
|
|
1141
|
+
const u = await getUpdateStatusAsync(true); // "지금 확인" — 신선한 캐시 무시하고 재조회
|
|
800
1142
|
if (u.latest === null) return ok('업데이트 확인에 실패했습니다(오프라인이거나 일시적 오류). 잠시 후 다시 시도하세요.', u);
|
|
1143
|
+
const user_message = u.update_available
|
|
1144
|
+
? `새 버전 v${u.latest}이 나왔어요. 현재 버전은 v${u.current}입니다. 원하시면 지금 업데이트할 수 있습니다.`
|
|
1145
|
+
: `최신 버전입니다 (v${u.current}).`;
|
|
801
1146
|
return ok(
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
1147
|
+
user_message,
|
|
1148
|
+
{
|
|
1149
|
+
...u,
|
|
1150
|
+
user_message,
|
|
1151
|
+
suggested_next_action: u.update_available
|
|
1152
|
+
? {
|
|
1153
|
+
prompt: '지금 CareerMate를 업데이트할까요?',
|
|
1154
|
+
next_tool: 'update_careermate',
|
|
1155
|
+
}
|
|
1156
|
+
: null,
|
|
1157
|
+
},
|
|
806
1158
|
);
|
|
807
1159
|
},
|
|
808
1160
|
},
|
|
@@ -810,12 +1162,12 @@ export const TOOLS: ToolDef[] = [
|
|
|
810
1162
|
name: 'update_careermate',
|
|
811
1163
|
title: 'CareerMate 업데이트',
|
|
812
1164
|
description:
|
|
813
|
-
'CareerMate를 최신 버전으로 업데이트합니다(설치 폴더에 npm으로 careermate@latest 설치). 사용자가 명시적으로 "업데이트해줘"라고 할
|
|
1165
|
+
'CareerMate를 최신 버전으로 업데이트합니다(설치 폴더에 npm으로 careermate@latest 설치). 사용자가 명시적으로 "업데이트해줘", "CareerMate 업데이트해줘", "최신으로 올려줘"라고 할 때 호출하세요. 이 도구가 최신 버전 확인도 함께 하므로 별도 확인 질문 없이 실행해도 됩니다. 성공하면 적용을 위해 AI 앱(또는 MCP 연결) 재시작이 필요하며, 재시작 시 데이터베이스는 자동 마이그레이션되어 기존 데이터는 보존됩니다. 자동 설치가 막히면(개발 모드·비표준 설치) 직접 칠 명령을 안내합니다.',
|
|
814
1166
|
inputSchema: {},
|
|
815
1167
|
handler: async () => {
|
|
816
1168
|
const r = await runSelfUpdate();
|
|
817
1169
|
if (!r.ok) return fail(`${r.message}${r.manual ? `\n\n직접 실행: ${r.manual}` : ''}`);
|
|
818
|
-
return ok(r.message, r);
|
|
1170
|
+
return ok(r.message, { ...r, user_message: r.message });
|
|
819
1171
|
},
|
|
820
1172
|
},
|
|
821
1173
|
];
|