careermate 0.1.0
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/README.md +256 -0
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/apps/mcp/src/index.ts +66 -0
- package/apps/web/DESIGN_GUIDE.md +105 -0
- package/apps/web/UI_CONTRACT.md +44 -0
- package/apps/web/public/app.js +118 -0
- package/apps/web/public/fonts/PretendardVariable.woff2 +0 -0
- package/apps/web/public/index.html +41 -0
- package/apps/web/public/lib.js +282 -0
- package/apps/web/public/pages/applications.js +98 -0
- package/apps/web/public/pages/documents.js +446 -0
- package/apps/web/public/pages/home.js +263 -0
- package/apps/web/public/pages/interview.js +230 -0
- package/apps/web/public/pages/jobs.js +494 -0
- package/apps/web/public/pages/profile.js +576 -0
- package/apps/web/public/pages/settings.js +233 -0
- package/apps/web/public/styles.css +426 -0
- package/apps/web/src/exports.ts +68 -0
- package/apps/web/src/http.ts +180 -0
- package/apps/web/src/index.ts +49 -0
- package/apps/web/src/info.ts +50 -0
- package/apps/web/src/routes.ts +350 -0
- package/apps/web/src/security.ts +102 -0
- package/apps/web/src/server.ts +141 -0
- package/apps/web/src/settings.ts +88 -0
- package/bin/careermate.mjs +74 -0
- package/dist/careermate.mcpb +0 -0
- package/dist/install-page/index.html +474 -0
- package/dist/install-page/style.css +391 -0
- package/dist/install-page/vercel.json +20 -0
- package/dist/mcp-smoke.err +3 -0
- package/dist/mcp.mjs +23704 -0
- package/dist/mcpb-stage/README.md +219 -0
- package/dist/mcpb-stage/dist/install-page/index.html +434 -0
- package/dist/mcpb-stage/dist/install-page/style.css +407 -0
- package/dist/mcpb-stage/dist/install-page/vercel.json +20 -0
- package/dist/mcpb-stage/dist/mcp.mjs +23704 -0
- package/dist/mcpb-stage/dist/public/app.js +118 -0
- package/dist/mcpb-stage/dist/public/fonts/PretendardVariable.woff2 +0 -0
- package/dist/mcpb-stage/dist/public/index.html +41 -0
- package/dist/mcpb-stage/dist/public/lib.js +282 -0
- package/dist/mcpb-stage/dist/public/pages/applications.js +98 -0
- package/dist/mcpb-stage/dist/public/pages/documents.js +446 -0
- package/dist/mcpb-stage/dist/public/pages/home.js +263 -0
- package/dist/mcpb-stage/dist/public/pages/interview.js +230 -0
- package/dist/mcpb-stage/dist/public/pages/jobs.js +494 -0
- package/dist/mcpb-stage/dist/public/pages/profile.js +576 -0
- package/dist/mcpb-stage/dist/public/pages/settings.js +233 -0
- package/dist/mcpb-stage/dist/public/styles.css +420 -0
- package/dist/mcpb-stage/dist/web.mjs +7240 -0
- package/dist/mcpb-stage/manifest.json +40 -0
- package/dist/public/app.js +118 -0
- package/dist/public/fonts/PretendardVariable.woff2 +0 -0
- package/dist/public/index.html +41 -0
- package/dist/public/lib.js +282 -0
- package/dist/public/pages/applications.js +98 -0
- package/dist/public/pages/documents.js +446 -0
- package/dist/public/pages/home.js +263 -0
- package/dist/public/pages/interview.js +230 -0
- package/dist/public/pages/jobs.js +494 -0
- package/dist/public/pages/profile.js +576 -0
- package/dist/public/pages/settings.js +233 -0
- package/dist/public/styles.css +426 -0
- package/dist/web.mjs +7240 -0
- package/docs/ARCHITECTURE.md +208 -0
- package/docs/CHANGES_V1.md +103 -0
- package/docs/DATA_MODEL.md +460 -0
- package/docs/DECISIONS.md +277 -0
- package/docs/DEMO.md +242 -0
- package/docs/INSTALL.md +148 -0
- package/docs/INSTALL_AND_USAGE.md +99 -0
- package/docs/MCP_TOOLS.md +233 -0
- package/docs/ROADMAP.md +134 -0
- package/docs/START_WORKFLOW.md +125 -0
- package/docs/SUPPORTED_AI_APPS.md +60 -0
- package/docs/TODO.md +57 -0
- package/docs/UX_NOTES.md +247 -0
- package/docs/WORKFLOWS.md +200 -0
- package/install-page/index.html +474 -0
- package/install-page/style.css +391 -0
- package/install-page/vercel.json +20 -0
- package/package.json +68 -0
- package/packages/core/src/context.ts +74 -0
- package/packages/core/src/index.ts +8 -0
- package/packages/core/src/onboarding.ts +81 -0
- package/packages/core/src/services.ts +146 -0
- package/packages/core/src/summary.ts +104 -0
- package/packages/db/src/connection.ts +46 -0
- package/packages/db/src/index.ts +22 -0
- package/packages/db/src/paths.ts +41 -0
- package/packages/db/src/repositories.ts +828 -0
- package/packages/db/src/runtime.ts +58 -0
- package/packages/db/src/schema.ts +189 -0
- package/packages/exporters/src/html.ts +113 -0
- package/packages/exporters/src/index.ts +364 -0
- package/packages/exporters/src/markdown.ts +178 -0
- package/packages/mcp-tools/src/bridge.ts +83 -0
- package/packages/mcp-tools/src/index.ts +8 -0
- package/packages/mcp-tools/src/result.ts +49 -0
- package/packages/mcp-tools/src/tools.ts +455 -0
- package/packages/parsers/src/html.ts +86 -0
- package/packages/parsers/src/index.ts +228 -0
- package/packages/parsers/src/keywords.ts +151 -0
- package/packages/prompts/src/humanize.ts +59 -0
- package/packages/prompts/src/index.ts +82 -0
- package/packages/prompts/src/install.ts +43 -0
- package/packages/prompts/src/onboarding.ts +35 -0
- package/packages/prompts/src/system.ts +53 -0
- package/packages/shared/src/enums.ts +103 -0
- package/packages/shared/src/index.ts +18 -0
- package/packages/shared/src/schemas.ts +398 -0
- package/packages/workflows/src/definitions.ts +107 -0
- package/packages/workflows/src/index.ts +39 -0
- package/scripts/build-dist.mjs +62 -0
- package/scripts/build-mcpb.mjs +70 -0
- package/scripts/doctor.ts +81 -0
- package/scripts/init.ts +342 -0
- package/scripts/mcp-probe.ts +55 -0
- package/scripts/migrate.ts +6 -0
- package/scripts/run.mjs +33 -0
- package/scripts/seed.ts +129 -0
- package/scripts/test.ts +117 -0
- package/scripts/ui-smoke.ts +73 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @careermate/parsers — extract clean text from pasted/uploaded content and
|
|
3
|
+
* normalize raw job postings.
|
|
4
|
+
*
|
|
5
|
+
* MVP scope: we handle plain text, markdown, and HTML directly. We deliberately
|
|
6
|
+
* do NOT parse binary formats (PDF/DOCX) — if such content arrives and is not
|
|
7
|
+
* already decodable text, we return a friendly warning asking the user to paste
|
|
8
|
+
* the text instead. Every function is pure and total: it never throws, even on
|
|
9
|
+
* empty or malformed input.
|
|
10
|
+
*/
|
|
11
|
+
import { stripHtml } from './html.ts';
|
|
12
|
+
import { extractTechKeywords } from './keywords.ts';
|
|
13
|
+
|
|
14
|
+
export { stripHtml, decodeEntities } from './html.ts';
|
|
15
|
+
export { extractTechKeywords, TECH_TERMS } from './keywords.ts';
|
|
16
|
+
export type { TechTerm } from './keywords.ts';
|
|
17
|
+
|
|
18
|
+
/* ----------------------------------------------------------------- extractText */
|
|
19
|
+
|
|
20
|
+
export interface ExtractTextInput {
|
|
21
|
+
filename?: string;
|
|
22
|
+
mimeType?: string;
|
|
23
|
+
content: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ExtractTextResult {
|
|
27
|
+
text: string;
|
|
28
|
+
/** Detected logical format: `text` | `markdown` | `html` | `unsupported`. */
|
|
29
|
+
format: string;
|
|
30
|
+
warnings: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const HTML_MIME_RE = /html/i;
|
|
34
|
+
const MD_EXT_RE = /\.(md|markdown|mdown|mkd)$/i;
|
|
35
|
+
const HTML_EXT_RE = /\.(html?|xhtml)$/i;
|
|
36
|
+
const BINARY_EXT_RE = /\.(pdf|docx?|hwpx?|pptx?|xlsx?|rtf|odt|pages|key)$/i;
|
|
37
|
+
const BINARY_MIME_RE = /(pdf|msword|officedocument|hancom|rtf|opendocument)/i;
|
|
38
|
+
|
|
39
|
+
/** Heuristic: does this content contain unprintable bytes (i.e. binary)? */
|
|
40
|
+
function looksBinary(s: string): boolean {
|
|
41
|
+
if (!s) return false;
|
|
42
|
+
// Sample the first chunk; count control chars (excluding tab/newline/CR).
|
|
43
|
+
const sample = s.slice(0, 2000);
|
|
44
|
+
let control = 0;
|
|
45
|
+
for (let i = 0; i < sample.length; i++) {
|
|
46
|
+
const c = sample.charCodeAt(i);
|
|
47
|
+
if (c === 0) return true; // NUL almost always means binary
|
|
48
|
+
if (c < 9 || (c > 13 && c < 32)) control++;
|
|
49
|
+
}
|
|
50
|
+
return control / sample.length > 0.1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Heuristic: does this text look like HTML markup? */
|
|
54
|
+
function looksLikeHtml(s: string): boolean {
|
|
55
|
+
return /<\/?[a-z][\s\S]*?>/i.test(s) && /<\/?(html|body|div|p|span|br|table|h[1-6]|ul|li)\b/i.test(s);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Extract readable text from pasted/uploaded content.
|
|
60
|
+
*
|
|
61
|
+
* - plain text / markdown → returned as-is (markdown is kept verbatim).
|
|
62
|
+
* - HTML (by mime, extension, or sniffing) → stripped to text.
|
|
63
|
+
* - PDF/DOCX/etc → if the payload is already decodable text we pass it through
|
|
64
|
+
* with a note; otherwise we warn and return an empty string.
|
|
65
|
+
*/
|
|
66
|
+
export function extractText(input: ExtractTextInput): ExtractTextResult {
|
|
67
|
+
const warnings: string[] = [];
|
|
68
|
+
const safe: ExtractTextInput = input ?? { content: '' };
|
|
69
|
+
const content = typeof safe.content === 'string' ? safe.content : '';
|
|
70
|
+
const filename = safe.filename ?? '';
|
|
71
|
+
const mimeType = safe.mimeType ?? '';
|
|
72
|
+
|
|
73
|
+
const isBinaryType = BINARY_EXT_RE.test(filename) || BINARY_MIME_RE.test(mimeType);
|
|
74
|
+
|
|
75
|
+
// Binary document formats: we don't parse them in the MVP.
|
|
76
|
+
if (isBinaryType) {
|
|
77
|
+
if (content && !looksBinary(content)) {
|
|
78
|
+
// Payload happens to already be plain text (e.g. pre-extracted).
|
|
79
|
+
warnings.push(
|
|
80
|
+
'바이너리 문서(PDF/DOCX 등)는 직접 파싱하지 않습니다. 이미 텍스트로 보이는 내용을 그대로 사용했습니다.',
|
|
81
|
+
);
|
|
82
|
+
return { text: content.trim(), format: 'unsupported', warnings };
|
|
83
|
+
}
|
|
84
|
+
warnings.push(
|
|
85
|
+
'PDF/DOCX 등 바이너리 문서는 현재 자동 추출을 지원하지 않습니다. 내용을 텍스트로 복사해 붙여넣어 주세요.',
|
|
86
|
+
);
|
|
87
|
+
return { text: '', format: 'unsupported', warnings };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Anything else that still smells binary -> warn.
|
|
91
|
+
if (looksBinary(content)) {
|
|
92
|
+
warnings.push(
|
|
93
|
+
'읽을 수 없는 바이너리 데이터로 보입니다. 내용을 텍스트로 복사해 붙여넣어 주세요.',
|
|
94
|
+
);
|
|
95
|
+
return { text: '', format: 'unsupported', warnings };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// HTML — by mime, extension, or content sniffing.
|
|
99
|
+
if (HTML_MIME_RE.test(mimeType) || HTML_EXT_RE.test(filename) || looksLikeHtml(content)) {
|
|
100
|
+
return { text: stripHtml(content), format: 'html', warnings };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Markdown — by extension or mime (kept verbatim; markdown IS readable text).
|
|
104
|
+
if (MD_EXT_RE.test(filename) || /markdown/i.test(mimeType)) {
|
|
105
|
+
return { text: content.trim(), format: 'markdown', warnings };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Default: plain text.
|
|
109
|
+
return { text: content.trim(), format: 'text', warnings };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* -------------------------------------------------------------- cleanJobPosting */
|
|
113
|
+
|
|
114
|
+
export interface CleanedJobPosting {
|
|
115
|
+
text: string;
|
|
116
|
+
company?: string;
|
|
117
|
+
position?: string;
|
|
118
|
+
deadline?: string;
|
|
119
|
+
keywords: string[];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Boilerplate-ish lines (nav, cookie banners, share buttons) we drop. */
|
|
123
|
+
const BOILERPLATE_RE =
|
|
124
|
+
/^(home|menu|login|로그인|회원가입|sign\s?up|sign\s?in|share|공유|스크랩|북마크|이전|다음|목록|prev(ious)?|next|cookie|쿠키|copyright|©|all rights reserved|이용약관|개인정보처리방침|상단으로|top|search|검색)$/i;
|
|
125
|
+
|
|
126
|
+
const COMPANY_LABEL_RE = /(?:회사명?|기업명?|company)\s*[::]\s*(.+)/i;
|
|
127
|
+
const POSITION_LABEL_RE = /(?:포지션|직무|모집\s*부문|채용\s*부문|position|job\s*title)\s*[::]\s*(.+)/i;
|
|
128
|
+
const DEADLINE_LABEL_RE =
|
|
129
|
+
/(?:마감(?:일|일자|기한)?|접수\s*마감|deadline|due)\s*[::]?\s*(.+)/i;
|
|
130
|
+
|
|
131
|
+
/** Pull a YYYY-MM-DD / YYYY.MM.DD / YYYY년 MM월 DD일 style date from text. */
|
|
132
|
+
function findDeadline(text: string): string | undefined {
|
|
133
|
+
// Explicit "상시채용" / "채용시 마감" style.
|
|
134
|
+
const rolling = /(상시\s*채용|채용\s*시\s*(?:마감|까지)|수시\s*채용)/i.exec(text);
|
|
135
|
+
|
|
136
|
+
// Look near deadline labels first for a concrete date.
|
|
137
|
+
for (const line of text.split('\n')) {
|
|
138
|
+
const m = DEADLINE_LABEL_RE.exec(line);
|
|
139
|
+
if (m) {
|
|
140
|
+
const date = normalizeDate(m[1]);
|
|
141
|
+
if (date) return date;
|
|
142
|
+
const tail = m[1].trim();
|
|
143
|
+
if (tail && tail.length <= 30) return tail;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Fall back to any date-looking token in the whole text.
|
|
148
|
+
const any = normalizeDate(text);
|
|
149
|
+
if (any) return any;
|
|
150
|
+
|
|
151
|
+
if (rolling) return rolling[1].replace(/\s+/g, ' ').trim();
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Normalize the first date found in `s` to `YYYY-MM-DD` (or `YYYY-MM`). */
|
|
156
|
+
function normalizeDate(s: string): string | undefined {
|
|
157
|
+
if (!s) return undefined;
|
|
158
|
+
// YYYY-MM-DD / YYYY.MM.DD / YYYY/MM/DD
|
|
159
|
+
let m = /(\d{4})[.\-/년]\s*(\d{1,2})[.\-/월]\s*(\d{1,2})/.exec(s);
|
|
160
|
+
if (m) {
|
|
161
|
+
const [, y, mo, d] = m;
|
|
162
|
+
return `${y}-${mo.padStart(2, '0')}-${d.padStart(2, '0')}`;
|
|
163
|
+
}
|
|
164
|
+
// YYYY-MM / YYYY.MM
|
|
165
|
+
m = /(\d{4})[.\-/년]\s*(\d{1,2})\b/.exec(s);
|
|
166
|
+
if (m) {
|
|
167
|
+
const [, y, mo] = m;
|
|
168
|
+
return `${y}-${mo.padStart(2, '0')}`;
|
|
169
|
+
}
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Find a labelled value (company/position) from the lines. */
|
|
174
|
+
function findLabelled(lines: string[], re: RegExp): string | undefined {
|
|
175
|
+
for (const line of lines) {
|
|
176
|
+
const m = re.exec(line);
|
|
177
|
+
if (m) {
|
|
178
|
+
const val = m[1].trim();
|
|
179
|
+
if (val && val.length <= 80) return val;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Normalize a raw job posting and best-effort extract structured hints.
|
|
187
|
+
* Never throws; on weird input it returns whatever it can (often just `text`
|
|
188
|
+
* and `keywords`).
|
|
189
|
+
*/
|
|
190
|
+
export function cleanJobPosting(raw: string): CleanedJobPosting {
|
|
191
|
+
const source = typeof raw === 'string' ? raw : '';
|
|
192
|
+
|
|
193
|
+
// If it looks like HTML, strip to text first.
|
|
194
|
+
const pre = /<\/?[a-z][\s\S]*?>/i.test(source) ? stripHtml(source) : source;
|
|
195
|
+
|
|
196
|
+
// Normalize whitespace per line and drop boilerplate-ish lines.
|
|
197
|
+
const rawLines = pre.replace(/\r\n?/g, '\n').split('\n');
|
|
198
|
+
const lines: string[] = [];
|
|
199
|
+
for (const original of rawLines) {
|
|
200
|
+
const line = original.replace(/[ \t\f\v]+/g, ' ').trim();
|
|
201
|
+
if (!line) {
|
|
202
|
+
// Preserve a single blank as a soft separator (collapsed later).
|
|
203
|
+
if (lines.length && lines[lines.length - 1] !== '') lines.push('');
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (BOILERPLATE_RE.test(line)) continue;
|
|
207
|
+
// Drop ultra-short noise lines that are pure separators.
|
|
208
|
+
if (/^[-=_*·•~]+$/.test(line)) continue;
|
|
209
|
+
lines.push(line);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Collapse leading/trailing blanks and runs.
|
|
213
|
+
while (lines.length && lines[0] === '') lines.shift();
|
|
214
|
+
while (lines.length && lines[lines.length - 1] === '') lines.pop();
|
|
215
|
+
|
|
216
|
+
const text = lines.join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
217
|
+
|
|
218
|
+
const company = findLabelled(lines, COMPANY_LABEL_RE);
|
|
219
|
+
const position = findLabelled(lines, POSITION_LABEL_RE);
|
|
220
|
+
const deadline = findDeadline(text);
|
|
221
|
+
const keywords = extractTechKeywords(text);
|
|
222
|
+
|
|
223
|
+
const result: CleanedJobPosting = { text, keywords };
|
|
224
|
+
if (company) result.company = company;
|
|
225
|
+
if (position) result.position = position;
|
|
226
|
+
if (deadline) result.deadline = deadline;
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @careermate/parsers — built-in tech keyword dictionary.
|
|
3
|
+
*
|
|
4
|
+
* A small, curated list of common technologies. Each entry maps a canonical
|
|
5
|
+
* display label to the regex-safe aliases we accept when scanning free text.
|
|
6
|
+
* Matching is word-boundary aware and case-insensitive. This is intentionally
|
|
7
|
+
* conservative: it favors precision over recall so we don't flood the user with
|
|
8
|
+
* noise.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface TechTerm {
|
|
12
|
+
/** Canonical label shown back to the user. */
|
|
13
|
+
label: string;
|
|
14
|
+
/** Alternate spellings (regex-escaped literally, no special chars expected). */
|
|
15
|
+
aliases?: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const TECH_TERMS: TechTerm[] = [
|
|
19
|
+
// Languages
|
|
20
|
+
{ label: 'JavaScript', aliases: ['js'] },
|
|
21
|
+
{ label: 'TypeScript', aliases: ['ts'] },
|
|
22
|
+
{ label: 'Python' },
|
|
23
|
+
{ label: 'Java' },
|
|
24
|
+
{ label: 'Kotlin' },
|
|
25
|
+
{ label: 'Go', aliases: ['golang'] },
|
|
26
|
+
{ label: 'Rust' },
|
|
27
|
+
{ label: 'C++', aliases: ['cpp'] },
|
|
28
|
+
{ label: 'C#', aliases: ['csharp'] },
|
|
29
|
+
{ label: 'C' },
|
|
30
|
+
{ label: 'Ruby' },
|
|
31
|
+
{ label: 'PHP' },
|
|
32
|
+
{ label: 'Swift' },
|
|
33
|
+
{ label: 'Scala' },
|
|
34
|
+
{ label: 'Dart' },
|
|
35
|
+
{ label: 'SQL' },
|
|
36
|
+
{ label: 'GraphQL' },
|
|
37
|
+
|
|
38
|
+
// Frontend frameworks/libs
|
|
39
|
+
{ label: 'React', aliases: ['react.js', 'reactjs'] },
|
|
40
|
+
{ label: 'Next.js', aliases: ['nextjs', 'next js'] },
|
|
41
|
+
{ label: 'Vue', aliases: ['vue.js', 'vuejs'] },
|
|
42
|
+
{ label: 'Nuxt', aliases: ['nuxt.js', 'nuxtjs'] },
|
|
43
|
+
{ label: 'Angular' },
|
|
44
|
+
{ label: 'Svelte' },
|
|
45
|
+
{ label: 'Redux' },
|
|
46
|
+
{ label: 'Tailwind', aliases: ['tailwindcss', 'tailwind css'] },
|
|
47
|
+
|
|
48
|
+
// Backend frameworks
|
|
49
|
+
{ label: 'Node.js', aliases: ['nodejs', 'node js', 'node'] },
|
|
50
|
+
{ label: 'Express', aliases: ['express.js', 'expressjs'] },
|
|
51
|
+
{ label: 'NestJS', aliases: ['nest.js', 'nest js'] },
|
|
52
|
+
{ label: 'Spring', aliases: ['spring boot', 'springboot'] },
|
|
53
|
+
{ label: 'Django' },
|
|
54
|
+
{ label: 'Flask' },
|
|
55
|
+
{ label: 'FastAPI' },
|
|
56
|
+
{ label: 'Rails', aliases: ['ruby on rails'] },
|
|
57
|
+
{ label: 'Laravel' },
|
|
58
|
+
{ label: '.NET', aliases: ['dotnet', 'asp.net'] },
|
|
59
|
+
|
|
60
|
+
// Datastores
|
|
61
|
+
{ label: 'PostgreSQL', aliases: ['postgres', 'postgresql'] },
|
|
62
|
+
{ label: 'MySQL' },
|
|
63
|
+
{ label: 'MariaDB' },
|
|
64
|
+
{ label: 'MongoDB', aliases: ['mongo'] },
|
|
65
|
+
{ label: 'Redis' },
|
|
66
|
+
{ label: 'Elasticsearch', aliases: ['elastic search'] },
|
|
67
|
+
{ label: 'Oracle' },
|
|
68
|
+
{ label: 'SQLite' },
|
|
69
|
+
{ label: 'Kafka' },
|
|
70
|
+
{ label: 'RabbitMQ' },
|
|
71
|
+
|
|
72
|
+
// Cloud / infra / devops
|
|
73
|
+
{ label: 'AWS', aliases: ['amazon web services'] },
|
|
74
|
+
{ label: 'GCP', aliases: ['google cloud'] },
|
|
75
|
+
{ label: 'Azure' },
|
|
76
|
+
{ label: 'Docker' },
|
|
77
|
+
{ label: 'Kubernetes', aliases: ['k8s'] },
|
|
78
|
+
{ label: 'Terraform' },
|
|
79
|
+
{ label: 'Jenkins' },
|
|
80
|
+
{ label: 'GitHub Actions', aliases: ['github action'] },
|
|
81
|
+
{ label: 'CI/CD', aliases: ['cicd'] },
|
|
82
|
+
{ label: 'Nginx' },
|
|
83
|
+
{ label: 'Linux' },
|
|
84
|
+
|
|
85
|
+
// Tooling / misc
|
|
86
|
+
{ label: 'Git' },
|
|
87
|
+
{ label: 'REST', aliases: ['rest api', 'restful'] },
|
|
88
|
+
{ label: 'gRPC' },
|
|
89
|
+
{ label: 'Webpack' },
|
|
90
|
+
{ label: 'Vite' },
|
|
91
|
+
{ label: 'Jest' },
|
|
92
|
+
{ label: 'Figma' },
|
|
93
|
+
{ label: 'Jira' },
|
|
94
|
+
|
|
95
|
+
// Data / ML
|
|
96
|
+
{ label: 'TensorFlow' },
|
|
97
|
+
{ label: 'PyTorch' },
|
|
98
|
+
{ label: 'Pandas' },
|
|
99
|
+
{ label: 'NumPy', aliases: ['numpy'] },
|
|
100
|
+
{ label: 'Spark', aliases: ['apache spark'] },
|
|
101
|
+
{ label: 'Airflow' },
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
/** Escape a literal string for safe inclusion in a RegExp. */
|
|
105
|
+
function escapeRegExp(s: string): string {
|
|
106
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Decide whether a term needs ASCII word boundaries. Terms with trailing
|
|
111
|
+
* special chars (C++, C#, .NET) can't use `\b` reliably, so we anchor on
|
|
112
|
+
* non-word-ish neighbors instead.
|
|
113
|
+
*/
|
|
114
|
+
function buildPattern(term: string): RegExp {
|
|
115
|
+
const esc = escapeRegExp(term);
|
|
116
|
+
// If the term is purely word characters, use real word boundaries.
|
|
117
|
+
if (/^[\w.]+$/.test(term) && /\w$/.test(term) && /^\w/.test(term)) {
|
|
118
|
+
return new RegExp(`(?<![\\w])${esc}(?![\\w])`, 'i');
|
|
119
|
+
}
|
|
120
|
+
// Otherwise require a non-letter/digit (or string edge) on each side so we
|
|
121
|
+
// don't match inside larger words.
|
|
122
|
+
return new RegExp(`(?<![A-Za-z0-9])${esc}(?![A-Za-z0-9])`, 'i');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Scan text and return canonical labels of tech terms found, de-duplicated and
|
|
127
|
+
* preserving the dictionary's order. Best-effort; never throws.
|
|
128
|
+
*/
|
|
129
|
+
export function extractTechKeywords(text: string, limit = 20): string[] {
|
|
130
|
+
if (!text) return [];
|
|
131
|
+
const found: string[] = [];
|
|
132
|
+
const seen = new Set<string>();
|
|
133
|
+
|
|
134
|
+
for (const term of TECH_TERMS) {
|
|
135
|
+
if (seen.has(term.label)) continue;
|
|
136
|
+
const candidates = [term.label, ...(term.aliases ?? [])];
|
|
137
|
+
const matched = candidates.some((c) => {
|
|
138
|
+
try {
|
|
139
|
+
return buildPattern(c).test(text);
|
|
140
|
+
} catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
if (matched) {
|
|
145
|
+
seen.add(term.label);
|
|
146
|
+
found.push(term.label);
|
|
147
|
+
if (found.length >= limit) break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return found;
|
|
151
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @careermate/prompts — humanize.ts
|
|
3
|
+
*
|
|
4
|
+
* HUMANIZE_WRITING_GUIDE — 자기소개서·자기 PR·지원 메일 등 '사람이 쓴 글'을 만들어야 하는
|
|
5
|
+
* 모든 작성 작업에서 AI가 따라야 할 한국어 글쓰기 규칙.
|
|
6
|
+
*
|
|
7
|
+
* CareerMate 안에는 LLM이 없으므로, 실제 글은 사용자의 AI가 씁니다. 이 가이드는 그 AI가
|
|
8
|
+
* "AI가 쓴 티"가 나지 않는 자연스러운 한국어를 쓰도록 작성 단계에서 직접 적용하는 규칙입니다.
|
|
9
|
+
*
|
|
10
|
+
* 출처: epoko77-ai/im-not-ai ("AI가 쓴 글이 아닌 것처럼 윤문해주는 스킬", MIT License)의
|
|
11
|
+
* 한국어 AI-tell 분류(번역투·기계적 병렬·클리셰 등)를 자기소개서 작성용으로 압축·각색했습니다.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const HUMANIZE_WRITING_GUIDE = `# 사람이 쓴 것처럼: 자기소개서·커리어 글쓰기 규칙
|
|
15
|
+
|
|
16
|
+
목표: 사실과 경험은 100% 그대로 두면서, "AI가 쓴 티"가 나는 문장 습관만 제거한다.
|
|
17
|
+
검열기를 속이는 게 아니라, 채용 담당자가 읽기에 자연스럽고 사람 냄새 나는 글을 쓰는 것이 목적이다.
|
|
18
|
+
|
|
19
|
+
## 절대 원칙
|
|
20
|
+
- 저장된 실제 경험·수치·회사명·고유명사·직접 인용은 절대 바꾸거나 지어내지 않는다. 문장 결만 다듬는다.
|
|
21
|
+
- 빈칸이나 확인이 필요한 부분은 매끄럽게 채워 넣지 말고 그대로 표시해 사용자에게 묻는다.
|
|
22
|
+
- 사용자의 선호 문체(preferred_tone)와 강조 포인트(emphasis_points)를 항상 우선한다.
|
|
23
|
+
|
|
24
|
+
## 피해야 할 AI-tell (작성하면서 곧바로 거른다)
|
|
25
|
+
|
|
26
|
+
1. 번역투 — 영어를 직역한 듯한 군더더기.
|
|
27
|
+
- "~에 대한", "~을 통해", "~에 있어서", "~라는 점에서", "가지고 있습니다", "~을 진행하였습니다"의 남용.
|
|
28
|
+
- "저는 ~한 사람입니다" 식 정의문 반복. → 행동·사실로 대체한다.
|
|
29
|
+
- 예: "성장에 대한 열정을 가지고 있습니다" → "더 나은 방법을 찾으면 그냥 못 참고 바꿔봅니다."
|
|
30
|
+
|
|
31
|
+
2. AI 클리셰·상투어 — 어떤 자소서에나 들어가는 공허한 문구.
|
|
32
|
+
- "급변하는 시대", "4차 산업혁명", "끊임없는 노력", "열정을 가지고", "귀사의 발전에 기여",
|
|
33
|
+
"최선을 다하겠습니다", "성장하는 인재", "소통과 협업" 같은 표현은 쓰지 않는다.
|
|
34
|
+
- 추상적 다짐 대신, 그 다짐을 증명하는 구체적 사건 하나를 쓴다.
|
|
35
|
+
|
|
36
|
+
3. 기계적 병렬구조 — 세 개씩 나열(rule of three), 모든 문단이 똑같은 골격.
|
|
37
|
+
- "첫째…둘째…셋째", "A하고 B하며 C하는" 같은 균일한 나열을 반복하지 않는다.
|
|
38
|
+
- 문단마다 길이와 시작 방식을 일부러 다르게 한다.
|
|
39
|
+
|
|
40
|
+
4. 상투적 연결어 남발 — "또한", "뿐만 아니라", "더 나아가", "이를 통해", "결론적으로".
|
|
41
|
+
- 접속어 없이 문장을 잇거나, 사건의 흐름 자체로 연결한다.
|
|
42
|
+
|
|
43
|
+
5. 균일한 문장 리듬 — 모든 문장이 비슷한 길이.
|
|
44
|
+
- 짧은 문장과 긴 문장을 섞는다. 가끔 아주 짧게 끊는다. 이렇게.
|
|
45
|
+
|
|
46
|
+
6. 과장된 수식·수동태 — "매우", "정말로", "탁월한", "성공적으로", "~되어졌습니다".
|
|
47
|
+
- 부사를 빼고 동사·명사를 구체화한다. 능동태를 기본으로 한다.
|
|
48
|
+
|
|
49
|
+
7. 공허한 일반론 — 수치·사례 없는 추상적 주장.
|
|
50
|
+
- "많은 성과를 냈습니다" → 저장된 실제 수치/결과로 바꾼다. 수치가 없으면 지어내지 말고 사용자에게 묻는다.
|
|
51
|
+
|
|
52
|
+
8. 형식 과용 — 불필요한 이모지, 과한 마크다운, em-dash(—) 남발, 굵게 처리 떡칠.
|
|
53
|
+
- 자기소개서 본문은 담백한 평문으로 쓴다.
|
|
54
|
+
|
|
55
|
+
## 마지막 자가 점검 (저장 전 한 번)
|
|
56
|
+
- 위 8가지 중 걸리는 문장이 있는가? 있으면 그 문장만 고친다.
|
|
57
|
+
- 소리 내어 읽었을 때 어색하거나 "사람이 이렇게 말 안 하는데" 싶은 곳을 다듬는다.
|
|
58
|
+
- 사실·수치·고유명사가 원본과 100% 일치하는가?
|
|
59
|
+
- 사용자의 톤과 강조 포인트가 살아 있는가?`;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @careermate/prompts — public entry point.
|
|
3
|
+
*
|
|
4
|
+
* Exports the well-structured prompt strings plus a small registry so the web
|
|
5
|
+
* app and MCP server can list, look up, and render the prompts.
|
|
6
|
+
*/
|
|
7
|
+
export { CAREERMATE_SYSTEM_PROMPT } from './system.ts';
|
|
8
|
+
export {
|
|
9
|
+
INSTALL_PROMPT_CLAUDE,
|
|
10
|
+
INSTALL_PROMPT_CHATGPT,
|
|
11
|
+
INSTALL_PROMPT_GENERIC,
|
|
12
|
+
} from './install.ts';
|
|
13
|
+
export { ONBOARDING_PROMPT } from './onboarding.ts';
|
|
14
|
+
export { HUMANIZE_WRITING_GUIDE } from './humanize.ts';
|
|
15
|
+
|
|
16
|
+
import { CAREERMATE_SYSTEM_PROMPT } from './system.ts';
|
|
17
|
+
import {
|
|
18
|
+
INSTALL_PROMPT_CLAUDE,
|
|
19
|
+
INSTALL_PROMPT_CHATGPT,
|
|
20
|
+
INSTALL_PROMPT_GENERIC,
|
|
21
|
+
} from './install.ts';
|
|
22
|
+
import { ONBOARDING_PROMPT } from './onboarding.ts';
|
|
23
|
+
import { HUMANIZE_WRITING_GUIDE } from './humanize.ts';
|
|
24
|
+
|
|
25
|
+
/** Which assistant / surface a prompt is meant for. */
|
|
26
|
+
export type PromptAudience = 'system' | 'claude' | 'chatgpt' | 'generic' | 'all';
|
|
27
|
+
|
|
28
|
+
export interface PromptEntry {
|
|
29
|
+
/** Stable machine id (used by getPrompt and the web/MCP listing). */
|
|
30
|
+
id: string;
|
|
31
|
+
/** Human-facing title (Korean). */
|
|
32
|
+
title: string;
|
|
33
|
+
/** Target assistant / surface. */
|
|
34
|
+
audience: PromptAudience;
|
|
35
|
+
/** The prompt body itself. */
|
|
36
|
+
body: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** All CareerMate prompts, listable by the web app and MCP server. */
|
|
40
|
+
export const PROMPTS: PromptEntry[] = [
|
|
41
|
+
{
|
|
42
|
+
id: 'system',
|
|
43
|
+
title: 'CareerMate 시스템 프롬프트 (AI 동작 방식)',
|
|
44
|
+
audience: 'system',
|
|
45
|
+
body: CAREERMATE_SYSTEM_PROMPT,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'install_claude',
|
|
49
|
+
title: 'Claude 설치 프롬프트',
|
|
50
|
+
audience: 'claude',
|
|
51
|
+
body: INSTALL_PROMPT_CLAUDE,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'install_chatgpt',
|
|
55
|
+
title: 'ChatGPT 설치 프롬프트',
|
|
56
|
+
audience: 'chatgpt',
|
|
57
|
+
body: INSTALL_PROMPT_CHATGPT,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'install_generic',
|
|
61
|
+
title: '범용 AI 설치 프롬프트',
|
|
62
|
+
audience: 'generic',
|
|
63
|
+
body: INSTALL_PROMPT_GENERIC,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'onboarding',
|
|
67
|
+
title: '온보딩 프롬프트 (연결 직후)',
|
|
68
|
+
audience: 'all',
|
|
69
|
+
body: ONBOARDING_PROMPT,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'humanize_writing',
|
|
73
|
+
title: 'AI 티 안 나는 글쓰기 가이드 (자기소개서 작성)',
|
|
74
|
+
audience: 'all',
|
|
75
|
+
body: HUMANIZE_WRITING_GUIDE,
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
/** Look up a prompt body by id. Returns undefined if the id is unknown. */
|
|
80
|
+
export function getPrompt(id: string): string | undefined {
|
|
81
|
+
return PROMPTS.find((p) => p.id === id)?.body;
|
|
82
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @careermate/prompts — install.ts
|
|
3
|
+
*
|
|
4
|
+
* Copy-paste install prompts the user pastes into their AI assistant to set up
|
|
5
|
+
* the CareerMate MCP connection, verify it, and begin onboarding.
|
|
6
|
+
*
|
|
7
|
+
* Three variants tuned to each assistant. All Korean-first.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const INSTALL_PROMPT_CLAUDE = `Claude야, 아래 CareerMate 설치 페이지(또는 INSTALL.md)를 읽고 내 컴퓨터에 CareerMate를 설치하고 설정해줘.
|
|
11
|
+
|
|
12
|
+
순서는 이렇게 진행해줘.
|
|
13
|
+
1. 설치 안내 페이지 / INSTALL.md 내용을 끝까지 읽고 그대로 따라 해.
|
|
14
|
+
2. CareerMate MCP 서버를 내 Claude에 연결(설정)해줘.
|
|
15
|
+
3. 연결이 끝나면 \`get_onboarding_status\`를 호출해서 정말로 연결되었는지 확인하고, 결과를 쉬운 말로 알려줘.
|
|
16
|
+
4. 그다음 나에게 이력서나 자기소개서 파일을 업로드하라고 안내해줘.
|
|
17
|
+
5. 내가 올린 파일을 바탕으로 내 프로필을 구조화해서 \`save_profile\`로 CareerMate에 저장하고, 온보딩을 시작해줘.
|
|
18
|
+
|
|
19
|
+
기술 용어는 최대한 빼고, 매 단계마다 지금 뭘 하고 있는지 쉬운 한국어로 알려줘. 막히는 부분이 있으면 추측하지 말고 나에게 물어봐줘.`;
|
|
20
|
+
|
|
21
|
+
export const INSTALL_PROMPT_CHATGPT = `ChatGPT야, 아래 페이지를 읽고 내 컴퓨터에 CareerMate를 설치하고 설정해줘. 설치가 끝나면 연결 상태를 확인하고, 나에게 이력서나 자기소개서 파일을 업로드하라고 안내해줘. 그다음 내 프로필을 구조화해서 CareerMate에 저장해줘.
|
|
22
|
+
|
|
23
|
+
구체적으로는 이렇게 해줘.
|
|
24
|
+
1. CareerMate 설치 안내 페이지 / INSTALL.md를 읽고 그대로 따라 설치해.
|
|
25
|
+
2. CareerMate MCP 커넥터를 연결(설정)해줘.
|
|
26
|
+
3. 연결 후 \`get_onboarding_status\`를 호출해 연결이 정상인지 확인하고 결과를 쉬운 말로 알려줘.
|
|
27
|
+
4. 이력서·자기소개서 파일을 업로드하라고 나에게 안내해줘.
|
|
28
|
+
5. 올린 파일로 내 프로필을 정리해서 \`save_profile\`로 저장하고 온보딩을 시작해줘.
|
|
29
|
+
|
|
30
|
+
진행 상황은 항상 쉬운 한국어로 설명하고, 확실하지 않은 정보는 지어내지 말고 나에게 물어봐줘.`;
|
|
31
|
+
|
|
32
|
+
export const INSTALL_PROMPT_GENERIC = `안녕, 아래 CareerMate 설치 안내(설치 페이지 또는 INSTALL.md)를 읽고 내 컴퓨터에 CareerMate를 설치하고 설정해줘.
|
|
33
|
+
|
|
34
|
+
CareerMate는 내 커리어 데이터(프로필·이력서·자기소개서·지원 현황)를 내 컴퓨터에 저장하고, MCP를 통해 너(내 AI)가 그 데이터를 읽고 쓸 수 있게 해주는 도구야. 분석과 작성은 전부 네가 해.
|
|
35
|
+
|
|
36
|
+
이렇게 진행해줘.
|
|
37
|
+
1. 설치 안내 문서를 끝까지 읽고 그대로 따라 설치해.
|
|
38
|
+
2. 네가 쓰는 AI 도구에 CareerMate MCP 서버를 연결(설정)해줘.
|
|
39
|
+
3. 연결 후 \`get_onboarding_status\`를 호출해 연결 상태를 확인하고, 결과를 쉬운 한국어로 알려줘.
|
|
40
|
+
4. 나에게 이력서나 자기소개서 파일을 업로드하라고 안내해줘.
|
|
41
|
+
5. 올린 파일로 내 프로필을 구조화해서 \`save_profile\`로 저장하고 온보딩을 시작해줘.
|
|
42
|
+
|
|
43
|
+
매 단계마다 쉬운 한국어로 설명하고, 불확실한 내용은 지어내지 말고 나에게 확인해줘.`;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @careermate/prompts — onboarding.ts
|
|
3
|
+
*
|
|
4
|
+
* ONBOARDING_PROMPT: what the AI should do right after connecting to CareerMate.
|
|
5
|
+
* Walks the user through profile / resume / cover-letter capture, then opens
|
|
6
|
+
* the dashboard. Korean-first.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const ONBOARDING_PROMPT = `이제 CareerMate에 연결되었습니다. 사용자의 커리어 데이터 첫 셋업(온보딩)을 도와주세요.
|
|
10
|
+
|
|
11
|
+
# 1단계 — 현재 상태 확인
|
|
12
|
+
- 먼저 \`get_onboarding_status\`를 호출하세요. 결과의 has_profile / has_resume / has_cover_letter / has_experience / has_skills / has_job, profile_completeness(완성도), next_steps(다음 단계)를 확인합니다.
|
|
13
|
+
- 사용자에게 "지금까지 등록된 정보"와 "앞으로 채우면 좋은 정보"를 쉬운 한국어로 짧게 안내하세요. (이미 채워진 항목은 다시 묻지 마세요.)
|
|
14
|
+
|
|
15
|
+
# 2단계 — 프로필 수집 및 저장
|
|
16
|
+
- 사용자가 올린 이력서/자기소개서 파일이나 사용자가 직접 알려준 내용을 바탕으로 프로필을 구조화하세요.
|
|
17
|
+
- 이름, 연락처, 한 줄 소개(headline), 자기소개 요약(summary), 희망 직무(desired_roles), 희망 근무 조건(desired_conditions)을 정리합니다.
|
|
18
|
+
- 글쓰기 선호도 함께 물어보세요: 선호 문체(preferred_tone, 예: "담백하고 구체적")와 강조하고 싶은 핵심 포인트(emphasis_points). 이 정보는 이후 자기소개서 작성에 그대로 활용됩니다.
|
|
19
|
+
- 정리한 내용을 \`save_profile\`로 저장하세요. 파일에서 읽어온 정보는 source를 'upload', 사용자가 말로 알려준 정보는 'manual'로 표시하세요.
|
|
20
|
+
- 경력·프로젝트·스킬 정보가 있으면 함께 정리해 프로필에 반영하세요(경력 achievements는 가능하면 정량 지표 포함).
|
|
21
|
+
- 확실하지 않은 정보는 지어내지 말고 사용자에게 확인하세요.
|
|
22
|
+
|
|
23
|
+
# 3단계 — 이력서 등록
|
|
24
|
+
- 업로드한 이력서/경력기술서/포트폴리오 본문을 \`add_resume\`로 저장하세요. kind(resume/career_description/portfolio/other)와 title을 적절히 지정하고, 대표 문서는 is_primary를 true로 설정하세요. source는 'upload'를 사용하세요.
|
|
25
|
+
|
|
26
|
+
# 4단계 — 자기소개서 등록 (있는 경우)
|
|
27
|
+
- 기존 자기소개서가 있으면 \`add_cover_letter\`로 저장하세요. 제목(title)과 본문(content)을 넣고, 특정 공고용이면 job_id를 연결하세요.
|
|
28
|
+
- 없으면 "나중에 공고를 분석한 뒤 맞춤 자기소개서를 써드릴 수 있어요"라고 안내만 하세요.
|
|
29
|
+
|
|
30
|
+
# 5단계 — 대시보드 열기 & 다음 단계
|
|
31
|
+
- \`open_dashboard\`를 호출해 사용자가 자기 데이터를 눈으로 확인하도록 하세요.
|
|
32
|
+
- 다시 \`get_onboarding_status\`로 완성도를 확인하고, 비어 있는 부분이 있으면 채우자고 제안하세요.
|
|
33
|
+
- 마무리로 다음 행동을 제안하세요. 예: "지원하고 싶은 공고가 있으면 링크나 내용을 붙여주세요. 적합도를 분석해드릴게요."
|
|
34
|
+
|
|
35
|
+
진행 내내 기술 용어(MCP, job_id 등)는 빼고, 지금 무엇을 하고 있는지 쉬운 한국어로 알려주세요.`;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @careermate/prompts — system.ts
|
|
3
|
+
*
|
|
4
|
+
* CAREERMATE_SYSTEM_PROMPT teaches an AI assistant (ChatGPT / Claude / Gemini)
|
|
5
|
+
* HOW to behave as a good CareerMate operator. CareerMate itself contains no LLM;
|
|
6
|
+
* the user's own AI does all reading, analysis, and writing through MCP tools.
|
|
7
|
+
*
|
|
8
|
+
* Written in Korean: it is read both by Korean users and by the AI on their behalf.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const CAREERMATE_SYSTEM_PROMPT = `당신은 사용자의 'CareerMate' 커리어 비서입니다.
|
|
12
|
+
|
|
13
|
+
CareerMate는 사용자의 컴퓨터에서 동작하는 로컬 커리어 관리 도구이며, MCP(툴 호출)를 통해 사용자의 프로필·이력서·자기소개서·지원 공고·지원 현황 데이터를 읽고 씁니다. CareerMate 안에는 별도의 AI가 없습니다. 모든 분석·작성·요약은 '당신'(사용자의 AI)이 MCP 툴을 호출해 수행합니다.
|
|
14
|
+
|
|
15
|
+
# 핵심 행동 원칙
|
|
16
|
+
|
|
17
|
+
1. 항상 맥락을 먼저 불러옵니다.
|
|
18
|
+
- 공고를 분석하거나 자기소개서를 쓰기 전에는 반드시 \`get_application_context\`를 먼저 호출하세요. 이 한 번의 호출로 프로필, 대표 이력서, 경력(experiences), 프로젝트(projects), 스킬(skills), 기존 자기소개서, 최근 지원 현황, 대상 공고(job_id 전달 시), 이전 적합도 분석, 같은 회사/직무 관련 기록, 그리고 사용자의 글쓰기 선호(선호 문체 preferred_tone + 강조 포인트 emphasis_points)를 한꺼번에 받습니다.
|
|
19
|
+
- 일반적인 상태 확인이 필요하면 \`get_onboarding_status\`, \`get_profile\`, \`get_resumes\`, \`get_cover_letters\`, \`list_recent_activity\` 등 읽기 툴을 적극 사용하세요.
|
|
20
|
+
- 기억에 의존하지 말고, 매 작업 시작 시 실제 데이터를 다시 불러오세요.
|
|
21
|
+
|
|
22
|
+
2. 결과는 반드시 다시 저장합니다.
|
|
23
|
+
- 분석·작성한 내용은 머릿속이나 대화창에만 남기지 말고 해당 저장 툴로 CareerMate에 기록하세요: 공고는 \`save_job_posting\`, 적합도 분석은 \`save_fit_analysis\`, 자기소개서 버전은 \`save_cover_letter_version\`, 면접 준비는 \`save_interview_prep\`, 상태 변경은 \`update_application_status\`.
|
|
24
|
+
- 저장 후에는 무엇이 저장되었는지 사용자에게 간단히 알려주세요.
|
|
25
|
+
|
|
26
|
+
3. 데이터를 지어내지 않습니다.
|
|
27
|
+
- 프로필·경력·성과·수치·회사명 등은 사용자가 제공했거나 CareerMate에 저장된 것만 사용하세요. 불확실하면 추측하지 말고 사용자에게 물어보세요.
|
|
28
|
+
- 자기소개서에 검증되지 않은 경험이나 과장된 성과를 넣지 마세요. 빈칸이 있으면 "이 부분은 확인이 필요합니다"라고 표시하고 사용자에게 확인을 요청하세요.
|
|
29
|
+
|
|
30
|
+
4. 로컬-퍼스트 / 프라이버시를 존중합니다.
|
|
31
|
+
- 모든 데이터는 사용자의 컴퓨터에만 저장됩니다. 데이터를 외부로 보내거나 다른 용도로 쓰지 마세요.
|
|
32
|
+
- 민감 정보(연락처, 주소 등)는 꼭 필요한 작업에만 사용하고 불필요하게 노출하지 마세요.
|
|
33
|
+
|
|
34
|
+
5. 자기소개서는 작성 전 확인을 받습니다.
|
|
35
|
+
- 자기소개서를 새로 쓰거나 크게 고치기 전에, 어떤 공고를 대상으로 어떤 톤과 강조점으로 쓸지 사용자에게 먼저 확인하세요.
|
|
36
|
+
- 작성 후에는 \`save_cover_letter_version\`으로 버전을 저장하고, 변경 요약을 \`note\`에 남기세요. 사용자가 원하면 \`export_cover_letter\`로 내보내세요.
|
|
37
|
+
|
|
38
|
+
6. 사용자에게는 쉬운 한국어로 알립니다.
|
|
39
|
+
- "MCP", "툴 호출", "job_id" 같은 기술 용어는 사용자에게 직접 노출하지 마세요. "공고를 저장했어요", "적합도를 분석했어요"처럼 일상적인 표현으로 진행 상황을 알리세요.
|
|
40
|
+
- 무엇을 했고, 무엇을 알게 되었고, 다음에 무엇을 할 수 있는지 짧고 명확하게 전달하세요.
|
|
41
|
+
|
|
42
|
+
7. 다음 단계를 먼저 제안합니다.
|
|
43
|
+
- 작업이 끝나면 자연스러운 다음 행동을 제안하세요. 예: 공고 분석 후 → "이 공고에 맞춘 자기소개서를 써드릴까요?", 자기소개서 저장 후 → "지원 상태를 '지원 완료'로 바꿀까요?", 상태가 '서류 합격'이 되면 → "면접 준비를 도와드릴까요?".
|
|
44
|
+
- \`get_onboarding_status\`의 next_steps나 \`open_dashboard\`/\`open_application\`을 활용해 사용자를 다음 행동으로 안내하세요.
|
|
45
|
+
|
|
46
|
+
8. 글은 AI 티 안 나게 씁니다.
|
|
47
|
+
- 자기소개서·자기 PR·지원 메일 등 사람이 쓴 듯한 글을 작성하기 직전에는 \`get_writing_style_guide\`를 호출해 글쓰기 규칙을 가져오고 그대로 적용하세요.
|
|
48
|
+
- 번역투, "열정을 가지고/귀사의 발전에 기여" 같은 클리셰, 기계적 병렬(첫째·둘째·셋째), 상투적 연결어, 균일한 문장 리듬을 피하고 담백하고 구체적으로 씁니다. 단, 사실·수치·고유명사는 절대 바꾸거나 지어내지 않습니다.
|
|
49
|
+
|
|
50
|
+
# 작업 방식 요약
|
|
51
|
+
- 읽기(get_*) → 분석/작성 → 저장(save_*/update_*) → 사용자에게 쉬운 말로 보고 → 다음 단계 제안.
|
|
52
|
+
- 8가지 지원 상태: 작성 중(draft), 지원 예정(planned), 지원 완료(applied), 서류 합격(document_passed), 면접 진행(interview), 최종 합격(final_passed), 불합격(rejected), 보류(on_hold).
|
|
53
|
+
- 의심스러우면 멈추고 묻습니다. 사용자의 커리어가 걸린 일이므로 정확함이 속도보다 중요합니다.`;
|