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,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CareerMate — shared enums and human-facing labels.
|
|
3
|
+
*
|
|
4
|
+
* These value sets are the single source of truth for both the MCP server
|
|
5
|
+
* (used by ChatGPT/Claude/Gemini) and the local web dashboard. Labels are in
|
|
6
|
+
* Korean because the primary audience is Korean job seekers; codes are stable
|
|
7
|
+
* machine values that must never change once persisted.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Application lifecycle. Drives the Applications board in the dashboard. */
|
|
11
|
+
export const APPLICATION_STATUSES = [
|
|
12
|
+
'draft', // 작성 중
|
|
13
|
+
'planned', // 지원 예정
|
|
14
|
+
'applied', // 지원 완료
|
|
15
|
+
'document_passed', // 서류 합격
|
|
16
|
+
'interview', // 면접 진행
|
|
17
|
+
'final_passed', // 최종 합격
|
|
18
|
+
'rejected', // 불합격
|
|
19
|
+
'on_hold', // 보류
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
export type ApplicationStatus = (typeof APPLICATION_STATUSES)[number];
|
|
23
|
+
|
|
24
|
+
export const APPLICATION_STATUS_LABELS: Record<ApplicationStatus, string> = {
|
|
25
|
+
draft: '작성 중',
|
|
26
|
+
planned: '지원 예정',
|
|
27
|
+
applied: '지원 완료',
|
|
28
|
+
document_passed: '서류 합격',
|
|
29
|
+
interview: '면접 진행',
|
|
30
|
+
final_passed: '최종 합격',
|
|
31
|
+
rejected: '불합격',
|
|
32
|
+
on_hold: '보류',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Ordered columns for the kanban-style Applications board. */
|
|
36
|
+
export const APPLICATION_BOARD_ORDER: ApplicationStatus[] = [
|
|
37
|
+
'draft',
|
|
38
|
+
'planned',
|
|
39
|
+
'applied',
|
|
40
|
+
'document_passed',
|
|
41
|
+
'interview',
|
|
42
|
+
'final_passed',
|
|
43
|
+
'on_hold',
|
|
44
|
+
'rejected',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/** Reaching this status unlocks interview-prep prompting in the workflow. */
|
|
48
|
+
export const INTERVIEW_UNLOCK_STATUSES: ApplicationStatus[] = [
|
|
49
|
+
'document_passed',
|
|
50
|
+
'interview',
|
|
51
|
+
'final_passed',
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
/** Stored document kinds (resumes and career descriptions). Cover letters live in their own tables. */
|
|
55
|
+
export const DOCUMENT_KINDS = ['resume', 'career_description', 'portfolio', 'other'] as const;
|
|
56
|
+
export type DocumentKind = (typeof DOCUMENT_KINDS)[number];
|
|
57
|
+
|
|
58
|
+
export const DOCUMENT_KIND_LABELS: Record<DocumentKind, string> = {
|
|
59
|
+
resume: '이력서',
|
|
60
|
+
career_description: '경력기술서',
|
|
61
|
+
portfolio: '포트폴리오',
|
|
62
|
+
other: '기타 문서',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** Where a piece of content originated — used to show provenance and protect privacy. */
|
|
66
|
+
export const CONTENT_SOURCES = ['manual', 'upload', 'ai', 'edit'] as const;
|
|
67
|
+
export type ContentSource = (typeof CONTENT_SOURCES)[number];
|
|
68
|
+
|
|
69
|
+
export const CONTENT_SOURCE_LABELS: Record<ContentSource, string> = {
|
|
70
|
+
manual: '직접 입력',
|
|
71
|
+
upload: '파일 업로드',
|
|
72
|
+
ai: 'AI 생성',
|
|
73
|
+
edit: '직접 수정',
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** Activity feed entry types (list_recent_activity). */
|
|
77
|
+
export const ACTIVITY_TYPES = [
|
|
78
|
+
'profile_updated',
|
|
79
|
+
'resume_added',
|
|
80
|
+
'cover_letter_added',
|
|
81
|
+
'cover_letter_version_saved',
|
|
82
|
+
'job_saved',
|
|
83
|
+
'fit_analysis_saved',
|
|
84
|
+
'application_status_changed',
|
|
85
|
+
'interview_prep_saved',
|
|
86
|
+
'document_exported',
|
|
87
|
+
] as const;
|
|
88
|
+
export type ActivityType = (typeof ACTIVITY_TYPES)[number];
|
|
89
|
+
|
|
90
|
+
/** Entity kinds referenced by activities and deep links. */
|
|
91
|
+
export const ENTITY_TYPES = [
|
|
92
|
+
'profile',
|
|
93
|
+
'experience',
|
|
94
|
+
'project',
|
|
95
|
+
'skill',
|
|
96
|
+
'document',
|
|
97
|
+
'cover_letter',
|
|
98
|
+
'job',
|
|
99
|
+
'application',
|
|
100
|
+
'fit_analysis',
|
|
101
|
+
'interview_prep',
|
|
102
|
+
] as const;
|
|
103
|
+
export type EntityType = (typeof ENTITY_TYPES)[number];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @careermate/shared — single source of truth for types, enums, and zod schemas
|
|
3
|
+
* shared by the DB layer, core use-cases, HTTP API, and MCP server.
|
|
4
|
+
*/
|
|
5
|
+
export * from './enums.ts';
|
|
6
|
+
export * from './schemas.ts';
|
|
7
|
+
|
|
8
|
+
/** Short, URL-safe, sortable-ish id. Avoids a uuid dependency. */
|
|
9
|
+
export function newId(prefix = ''): string {
|
|
10
|
+
const rnd = Math.random().toString(36).slice(2, 10);
|
|
11
|
+
const time = Date.now().toString(36);
|
|
12
|
+
return `${prefix}${time}${rnd}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Current timestamp as ISO-8601 string (stored verbatim in SQLite). */
|
|
16
|
+
export function now(): string {
|
|
17
|
+
return new Date().toISOString();
|
|
18
|
+
}
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CareerMate — zod schemas + inferred TypeScript types.
|
|
3
|
+
*
|
|
4
|
+
* Two layers per entity:
|
|
5
|
+
* - `*Input` : what the MCP tools / HTTP API accept from the AI or dashboard.
|
|
6
|
+
* - `*Record` : the persisted shape returned from the DB (adds id + timestamps).
|
|
7
|
+
*
|
|
8
|
+
* JSON-ish nested fields (achievements, tech, strengths…) are stored as JSON
|
|
9
|
+
* text columns in SQLite and parsed back into arrays/objects by the repositories.
|
|
10
|
+
*/
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import {
|
|
13
|
+
APPLICATION_STATUSES,
|
|
14
|
+
ACTIVITY_TYPES,
|
|
15
|
+
CONTENT_SOURCES,
|
|
16
|
+
DOCUMENT_KINDS,
|
|
17
|
+
ENTITY_TYPES,
|
|
18
|
+
} from './enums.ts';
|
|
19
|
+
|
|
20
|
+
const isoDateTime = z.string();
|
|
21
|
+
const id = z.string();
|
|
22
|
+
|
|
23
|
+
const baseRecord = {
|
|
24
|
+
id,
|
|
25
|
+
created_at: isoDateTime,
|
|
26
|
+
updated_at: isoDateTime,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/* ------------------------------------------------------------------ Profile */
|
|
30
|
+
|
|
31
|
+
export const LinkSchema = z.object({
|
|
32
|
+
label: z.string().min(1),
|
|
33
|
+
url: z.string().min(1),
|
|
34
|
+
});
|
|
35
|
+
export type Link = z.infer<typeof LinkSchema>;
|
|
36
|
+
|
|
37
|
+
export const ProfileInputSchema = z.object({
|
|
38
|
+
name: z.string().min(1).optional(),
|
|
39
|
+
email: z.string().optional(),
|
|
40
|
+
phone: z.string().optional(),
|
|
41
|
+
location: z.string().optional(),
|
|
42
|
+
headline: z.string().optional().describe('한 줄 소개 / 직무 타이틀'),
|
|
43
|
+
summary: z.string().optional().describe('자기소개 요약'),
|
|
44
|
+
desired_roles: z.array(z.string()).optional().describe('희망 직무'),
|
|
45
|
+
desired_conditions: z.string().optional().describe('희망 근무 조건 (연봉, 지역, 근무형태 등)'),
|
|
46
|
+
preferred_tone: z.string().optional().describe('자기소개서 선호 문체 (예: 담백하고 구체적)'),
|
|
47
|
+
emphasis_points: z.array(z.string()).optional().describe('강조하고 싶은 핵심 포인트'),
|
|
48
|
+
links: z.array(LinkSchema).optional().describe('포트폴리오/깃허브/링크드인 등'),
|
|
49
|
+
});
|
|
50
|
+
export type ProfileInput = z.infer<typeof ProfileInputSchema>;
|
|
51
|
+
|
|
52
|
+
export const ProfileRecordSchema = z.object({
|
|
53
|
+
...baseRecord,
|
|
54
|
+
name: z.string().nullable(),
|
|
55
|
+
email: z.string().nullable(),
|
|
56
|
+
phone: z.string().nullable(),
|
|
57
|
+
location: z.string().nullable(),
|
|
58
|
+
headline: z.string().nullable(),
|
|
59
|
+
summary: z.string().nullable(),
|
|
60
|
+
desired_roles: z.array(z.string()),
|
|
61
|
+
desired_conditions: z.string().nullable(),
|
|
62
|
+
preferred_tone: z.string().nullable(),
|
|
63
|
+
emphasis_points: z.array(z.string()),
|
|
64
|
+
links: z.array(LinkSchema),
|
|
65
|
+
});
|
|
66
|
+
export type ProfileRecord = z.infer<typeof ProfileRecordSchema>;
|
|
67
|
+
|
|
68
|
+
/* --------------------------------------------------------------- Experience */
|
|
69
|
+
|
|
70
|
+
export const ExperienceInputSchema = z.object({
|
|
71
|
+
company: z.string().min(1),
|
|
72
|
+
role: z.string().optional(),
|
|
73
|
+
employment_type: z.string().optional().describe('정규직/계약직/인턴/프리랜서 등'),
|
|
74
|
+
start_date: z.string().optional().describe('YYYY-MM 또는 YYYY-MM-DD'),
|
|
75
|
+
end_date: z.string().optional().describe('재직 중이면 비워두기'),
|
|
76
|
+
is_current: z.boolean().optional(),
|
|
77
|
+
description: z.string().optional(),
|
|
78
|
+
achievements: z.array(z.string()).optional().describe('성과/업적 (정량 지표 권장)'),
|
|
79
|
+
tech: z.array(z.string()).optional().describe('사용 기술/도구'),
|
|
80
|
+
order_index: z.number().optional(),
|
|
81
|
+
});
|
|
82
|
+
export type ExperienceInput = z.infer<typeof ExperienceInputSchema>;
|
|
83
|
+
|
|
84
|
+
export const ExperienceRecordSchema = z.object({
|
|
85
|
+
...baseRecord,
|
|
86
|
+
company: z.string(),
|
|
87
|
+
role: z.string().nullable(),
|
|
88
|
+
employment_type: z.string().nullable(),
|
|
89
|
+
start_date: z.string().nullable(),
|
|
90
|
+
end_date: z.string().nullable(),
|
|
91
|
+
is_current: z.boolean(),
|
|
92
|
+
description: z.string().nullable(),
|
|
93
|
+
achievements: z.array(z.string()),
|
|
94
|
+
tech: z.array(z.string()),
|
|
95
|
+
order_index: z.number(),
|
|
96
|
+
});
|
|
97
|
+
export type ExperienceRecord = z.infer<typeof ExperienceRecordSchema>;
|
|
98
|
+
|
|
99
|
+
/* ------------------------------------------------------------------ Project */
|
|
100
|
+
|
|
101
|
+
export const ProjectInputSchema = z.object({
|
|
102
|
+
name: z.string().min(1),
|
|
103
|
+
role: z.string().optional(),
|
|
104
|
+
description: z.string().optional(),
|
|
105
|
+
highlights: z.array(z.string()).optional(),
|
|
106
|
+
tech: z.array(z.string()).optional(),
|
|
107
|
+
url: z.string().optional(),
|
|
108
|
+
start_date: z.string().optional(),
|
|
109
|
+
end_date: z.string().optional(),
|
|
110
|
+
order_index: z.number().optional(),
|
|
111
|
+
});
|
|
112
|
+
export type ProjectInput = z.infer<typeof ProjectInputSchema>;
|
|
113
|
+
|
|
114
|
+
export const ProjectRecordSchema = z.object({
|
|
115
|
+
...baseRecord,
|
|
116
|
+
name: z.string(),
|
|
117
|
+
role: z.string().nullable(),
|
|
118
|
+
description: z.string().nullable(),
|
|
119
|
+
highlights: z.array(z.string()),
|
|
120
|
+
tech: z.array(z.string()),
|
|
121
|
+
url: z.string().nullable(),
|
|
122
|
+
start_date: z.string().nullable(),
|
|
123
|
+
end_date: z.string().nullable(),
|
|
124
|
+
order_index: z.number(),
|
|
125
|
+
});
|
|
126
|
+
export type ProjectRecord = z.infer<typeof ProjectRecordSchema>;
|
|
127
|
+
|
|
128
|
+
/* -------------------------------------------------------------------- Skill */
|
|
129
|
+
|
|
130
|
+
export const SkillInputSchema = z.object({
|
|
131
|
+
name: z.string().min(1),
|
|
132
|
+
category: z.string().optional().describe('언어/프레임워크/툴/소프트스킬 등'),
|
|
133
|
+
level: z.string().optional().describe('상/중/하 또는 자유 서술'),
|
|
134
|
+
years: z.number().optional(),
|
|
135
|
+
order_index: z.number().optional(),
|
|
136
|
+
});
|
|
137
|
+
export type SkillInput = z.infer<typeof SkillInputSchema>;
|
|
138
|
+
|
|
139
|
+
export const SkillRecordSchema = z.object({
|
|
140
|
+
...baseRecord,
|
|
141
|
+
name: z.string(),
|
|
142
|
+
category: z.string().nullable(),
|
|
143
|
+
level: z.string().nullable(),
|
|
144
|
+
years: z.number().nullable(),
|
|
145
|
+
order_index: z.number(),
|
|
146
|
+
});
|
|
147
|
+
export type SkillRecord = z.infer<typeof SkillRecordSchema>;
|
|
148
|
+
|
|
149
|
+
/* ----------------------------------------------------------------- Document */
|
|
150
|
+
|
|
151
|
+
export const DocumentInputSchema = z.object({
|
|
152
|
+
kind: z.enum(DOCUMENT_KINDS),
|
|
153
|
+
title: z.string().min(1),
|
|
154
|
+
content: z.string().describe('Markdown 또는 일반 텍스트 본문'),
|
|
155
|
+
source: z.enum(CONTENT_SOURCES).optional(),
|
|
156
|
+
is_primary: z.boolean().optional().describe('대표 문서 여부'),
|
|
157
|
+
tags: z.array(z.string()).optional(),
|
|
158
|
+
});
|
|
159
|
+
export type DocumentInput = z.infer<typeof DocumentInputSchema>;
|
|
160
|
+
|
|
161
|
+
export const DocumentRecordSchema = z.object({
|
|
162
|
+
...baseRecord,
|
|
163
|
+
kind: z.enum(DOCUMENT_KINDS),
|
|
164
|
+
title: z.string(),
|
|
165
|
+
content: z.string(),
|
|
166
|
+
source: z.enum(CONTENT_SOURCES),
|
|
167
|
+
is_primary: z.boolean(),
|
|
168
|
+
tags: z.array(z.string()),
|
|
169
|
+
});
|
|
170
|
+
export type DocumentRecord = z.infer<typeof DocumentRecordSchema>;
|
|
171
|
+
|
|
172
|
+
/* -------------------------------------------------------------- CoverLetter */
|
|
173
|
+
|
|
174
|
+
export const CoverLetterInputSchema = z.object({
|
|
175
|
+
title: z.string().min(1),
|
|
176
|
+
job_id: z.string().optional().describe('특정 공고에 연결할 경우'),
|
|
177
|
+
is_primary: z.boolean().optional(),
|
|
178
|
+
content: z.string().optional().describe('초기 버전 본문 (있으면 v1로 저장)'),
|
|
179
|
+
source: z.enum(CONTENT_SOURCES).optional(),
|
|
180
|
+
note: z.string().optional().describe('초기 버전 메모'),
|
|
181
|
+
});
|
|
182
|
+
export type CoverLetterInput = z.infer<typeof CoverLetterInputSchema>;
|
|
183
|
+
|
|
184
|
+
export const CoverLetterVersionInputSchema = z.object({
|
|
185
|
+
cover_letter_id: z.string().optional().describe('기존 자기소개서에 새 버전 추가 시'),
|
|
186
|
+
title: z.string().optional().describe('새 자기소개서를 만들 때 제목'),
|
|
187
|
+
job_id: z.string().optional(),
|
|
188
|
+
content: z.string().min(1),
|
|
189
|
+
note: z.string().optional().describe('이 버전의 변경 요약 (예: 지원동기 보강)'),
|
|
190
|
+
source: z.enum(CONTENT_SOURCES).optional(),
|
|
191
|
+
set_current: z.boolean().optional().describe('이 버전을 현재 버전으로 지정 (기본 true)'),
|
|
192
|
+
});
|
|
193
|
+
export type CoverLetterVersionInput = z.infer<typeof CoverLetterVersionInputSchema>;
|
|
194
|
+
|
|
195
|
+
export const CoverLetterVersionRecordSchema = z.object({
|
|
196
|
+
id,
|
|
197
|
+
cover_letter_id: z.string(),
|
|
198
|
+
version_no: z.number(),
|
|
199
|
+
content: z.string(),
|
|
200
|
+
note: z.string().nullable(),
|
|
201
|
+
source: z.enum(CONTENT_SOURCES),
|
|
202
|
+
created_at: isoDateTime,
|
|
203
|
+
});
|
|
204
|
+
export type CoverLetterVersionRecord = z.infer<typeof CoverLetterVersionRecordSchema>;
|
|
205
|
+
|
|
206
|
+
export const CoverLetterRecordSchema = z.object({
|
|
207
|
+
...baseRecord,
|
|
208
|
+
title: z.string(),
|
|
209
|
+
job_id: z.string().nullable(),
|
|
210
|
+
is_primary: z.boolean(),
|
|
211
|
+
current_version_id: z.string().nullable(),
|
|
212
|
+
version_count: z.number(),
|
|
213
|
+
current_content: z.string().nullable(),
|
|
214
|
+
versions: z.array(CoverLetterVersionRecordSchema).optional(),
|
|
215
|
+
});
|
|
216
|
+
export type CoverLetterRecord = z.infer<typeof CoverLetterRecordSchema>;
|
|
217
|
+
|
|
218
|
+
/* --------------------------------------------------------------------- Job */
|
|
219
|
+
|
|
220
|
+
export const JobInputSchema = z.object({
|
|
221
|
+
company: z.string().min(1),
|
|
222
|
+
position: z.string().min(1).describe('직무/포지션명'),
|
|
223
|
+
url: z.string().optional(),
|
|
224
|
+
location: z.string().optional(),
|
|
225
|
+
employment_type: z.string().optional(),
|
|
226
|
+
description: z.string().optional().describe('공고 원문 또는 정리된 텍스트'),
|
|
227
|
+
requirements: z.array(z.string()).optional().describe('자격요건/우대사항 핵심'),
|
|
228
|
+
keywords: z.array(z.string()).optional().describe('핵심 키워드'),
|
|
229
|
+
deadline: z.string().optional().describe('마감일 YYYY-MM-DD'),
|
|
230
|
+
source: z.string().optional().describe('출처 (사람인/원티드/직접 입력 등)'),
|
|
231
|
+
});
|
|
232
|
+
export type JobInput = z.infer<typeof JobInputSchema>;
|
|
233
|
+
|
|
234
|
+
export const JobRecordSchema = z.object({
|
|
235
|
+
...baseRecord,
|
|
236
|
+
company: z.string(),
|
|
237
|
+
position: z.string(),
|
|
238
|
+
url: z.string().nullable(),
|
|
239
|
+
location: z.string().nullable(),
|
|
240
|
+
employment_type: z.string().nullable(),
|
|
241
|
+
description: z.string().nullable(),
|
|
242
|
+
requirements: z.array(z.string()),
|
|
243
|
+
keywords: z.array(z.string()),
|
|
244
|
+
deadline: z.string().nullable(),
|
|
245
|
+
source: z.string().nullable(),
|
|
246
|
+
});
|
|
247
|
+
export type JobRecord = z.infer<typeof JobRecordSchema>;
|
|
248
|
+
|
|
249
|
+
/* ------------------------------------------------------------- FitAnalysis */
|
|
250
|
+
|
|
251
|
+
export const FitAnalysisInputSchema = z.object({
|
|
252
|
+
job_id: z.string().describe('분석 대상 공고 ID'),
|
|
253
|
+
score: z.number().min(0).max(100).optional().describe('종합 적합도 0~100'),
|
|
254
|
+
summary: z.string().optional().describe('한두 문단 요약'),
|
|
255
|
+
strengths: z.array(z.string()).optional().describe('강점 / 잘 맞는 부분'),
|
|
256
|
+
gaps: z.array(z.string()).optional().describe('부족한 부분 / 보완 필요'),
|
|
257
|
+
matched_keywords: z.array(z.string()).optional(),
|
|
258
|
+
missing_keywords: z.array(z.string()).optional(),
|
|
259
|
+
recommendations: z.array(z.string()).optional().describe('자기소개서/지원 전략 제안'),
|
|
260
|
+
});
|
|
261
|
+
export type FitAnalysisInput = z.infer<typeof FitAnalysisInputSchema>;
|
|
262
|
+
|
|
263
|
+
export const FitAnalysisRecordSchema = z.object({
|
|
264
|
+
...baseRecord,
|
|
265
|
+
job_id: z.string(),
|
|
266
|
+
score: z.number().nullable(),
|
|
267
|
+
summary: z.string().nullable(),
|
|
268
|
+
strengths: z.array(z.string()),
|
|
269
|
+
gaps: z.array(z.string()),
|
|
270
|
+
matched_keywords: z.array(z.string()),
|
|
271
|
+
missing_keywords: z.array(z.string()),
|
|
272
|
+
recommendations: z.array(z.string()),
|
|
273
|
+
});
|
|
274
|
+
export type FitAnalysisRecord = z.infer<typeof FitAnalysisRecordSchema>;
|
|
275
|
+
|
|
276
|
+
/* ------------------------------------------------------------- Application */
|
|
277
|
+
|
|
278
|
+
export const ApplicationInputSchema = z.object({
|
|
279
|
+
job_id: z.string(),
|
|
280
|
+
status: z.enum(APPLICATION_STATUSES).optional(),
|
|
281
|
+
resume_id: z.string().optional(),
|
|
282
|
+
cover_letter_id: z.string().optional(),
|
|
283
|
+
applied_at: z.string().optional(),
|
|
284
|
+
notes: z.string().optional(),
|
|
285
|
+
});
|
|
286
|
+
export type ApplicationInput = z.infer<typeof ApplicationInputSchema>;
|
|
287
|
+
|
|
288
|
+
export const ApplicationStatusUpdateSchema = z.object({
|
|
289
|
+
job_id: z.string().optional(),
|
|
290
|
+
application_id: z.string().optional(),
|
|
291
|
+
status: z.enum(APPLICATION_STATUSES),
|
|
292
|
+
note: z.string().optional().describe('상태 변경 사유/메모'),
|
|
293
|
+
});
|
|
294
|
+
export type ApplicationStatusUpdate = z.infer<typeof ApplicationStatusUpdateSchema>;
|
|
295
|
+
|
|
296
|
+
export const ApplicationRecordSchema = z.object({
|
|
297
|
+
...baseRecord,
|
|
298
|
+
job_id: z.string(),
|
|
299
|
+
status: z.enum(APPLICATION_STATUSES),
|
|
300
|
+
resume_id: z.string().nullable(),
|
|
301
|
+
cover_letter_id: z.string().nullable(),
|
|
302
|
+
applied_at: z.string().nullable(),
|
|
303
|
+
notes: z.string().nullable(),
|
|
304
|
+
});
|
|
305
|
+
export type ApplicationRecord = z.infer<typeof ApplicationRecordSchema>;
|
|
306
|
+
|
|
307
|
+
/* ----------------------------------------------------------- InterviewPrep */
|
|
308
|
+
|
|
309
|
+
export const StarGuideSchema = z.object({
|
|
310
|
+
question: z.string(),
|
|
311
|
+
situation: z.string().optional(),
|
|
312
|
+
task: z.string().optional(),
|
|
313
|
+
action: z.string().optional(),
|
|
314
|
+
result: z.string().optional(),
|
|
315
|
+
});
|
|
316
|
+
export type StarGuide = z.infer<typeof StarGuideSchema>;
|
|
317
|
+
|
|
318
|
+
export const InterviewQuestionSchema = z.object({
|
|
319
|
+
question: z.string(),
|
|
320
|
+
intent: z.string().optional().describe('질문 의도'),
|
|
321
|
+
followups: z.array(z.string()).optional().describe('예상 꼬리 질문'),
|
|
322
|
+
answer_outline: z.string().optional().describe('답변 가이드/핵심 포인트'),
|
|
323
|
+
});
|
|
324
|
+
export type InterviewQuestion = z.infer<typeof InterviewQuestionSchema>;
|
|
325
|
+
|
|
326
|
+
export const InterviewPrepInputSchema = z.object({
|
|
327
|
+
job_id: z.string(),
|
|
328
|
+
questions: z.array(InterviewQuestionSchema).optional(),
|
|
329
|
+
star_guides: z.array(StarGuideSchema).optional(),
|
|
330
|
+
self_introduction: z.string().optional().describe('1분 자기소개 초안'),
|
|
331
|
+
notes: z.string().optional().describe('면접 후기/메모'),
|
|
332
|
+
});
|
|
333
|
+
export type InterviewPrepInput = z.infer<typeof InterviewPrepInputSchema>;
|
|
334
|
+
|
|
335
|
+
export const InterviewPrepRecordSchema = z.object({
|
|
336
|
+
...baseRecord,
|
|
337
|
+
job_id: z.string(),
|
|
338
|
+
questions: z.array(InterviewQuestionSchema),
|
|
339
|
+
star_guides: z.array(StarGuideSchema),
|
|
340
|
+
self_introduction: z.string().nullable(),
|
|
341
|
+
notes: z.string().nullable(),
|
|
342
|
+
});
|
|
343
|
+
export type InterviewPrepRecord = z.infer<typeof InterviewPrepRecordSchema>;
|
|
344
|
+
|
|
345
|
+
/* ----------------------------------------------------------------- Activity */
|
|
346
|
+
|
|
347
|
+
export const ActivityRecordSchema = z.object({
|
|
348
|
+
id,
|
|
349
|
+
type: z.enum(ACTIVITY_TYPES),
|
|
350
|
+
entity_type: z.enum(ENTITY_TYPES).nullable(),
|
|
351
|
+
entity_id: z.string().nullable(),
|
|
352
|
+
summary: z.string(),
|
|
353
|
+
created_at: isoDateTime,
|
|
354
|
+
});
|
|
355
|
+
export type ActivityRecord = z.infer<typeof ActivityRecordSchema>;
|
|
356
|
+
|
|
357
|
+
/* ----------------------------------------------------- Aggregates / context */
|
|
358
|
+
|
|
359
|
+
export const OnboardingStatusSchema = z.object({
|
|
360
|
+
completed: z.boolean(),
|
|
361
|
+
has_profile: z.boolean(),
|
|
362
|
+
has_resume: z.boolean(),
|
|
363
|
+
has_cover_letter: z.boolean(),
|
|
364
|
+
has_experience: z.boolean(),
|
|
365
|
+
has_skills: z.boolean(),
|
|
366
|
+
has_job: z.boolean(),
|
|
367
|
+
profile_completeness: z.number().describe('0~100 프로필 완성도'),
|
|
368
|
+
next_steps: z.array(z.string()).describe('지금 해야 할 다음 단계 안내'),
|
|
369
|
+
});
|
|
370
|
+
export type OnboardingStatus = z.infer<typeof OnboardingStatusSchema>;
|
|
371
|
+
|
|
372
|
+
/** The one-shot context bundle the AI pulls before analyzing a job or writing. */
|
|
373
|
+
export const ApplicationContextSchema = z.object({
|
|
374
|
+
profile: ProfileRecordSchema.nullable(),
|
|
375
|
+
primary_resume: DocumentRecordSchema.nullable(),
|
|
376
|
+
resumes: z.array(DocumentRecordSchema),
|
|
377
|
+
experiences: z.array(ExperienceRecordSchema),
|
|
378
|
+
projects: z.array(ProjectRecordSchema),
|
|
379
|
+
skills: z.array(SkillRecordSchema),
|
|
380
|
+
cover_letters: z.array(CoverLetterRecordSchema),
|
|
381
|
+
recent_applications: z.array(ApplicationRecordSchema),
|
|
382
|
+
job: JobRecordSchema.nullable(),
|
|
383
|
+
fit_analysis: FitAnalysisRecordSchema.nullable(),
|
|
384
|
+
related_history: z
|
|
385
|
+
.array(
|
|
386
|
+
z.object({
|
|
387
|
+
job: JobRecordSchema,
|
|
388
|
+
application: ApplicationRecordSchema.nullable(),
|
|
389
|
+
fit_analysis: FitAnalysisRecordSchema.nullable(),
|
|
390
|
+
}),
|
|
391
|
+
)
|
|
392
|
+
.describe('같은 회사/직무 관련 이전 기록'),
|
|
393
|
+
writing_preferences: z.object({
|
|
394
|
+
preferred_tone: z.string().nullable(),
|
|
395
|
+
emphasis_points: z.array(z.string()),
|
|
396
|
+
}),
|
|
397
|
+
});
|
|
398
|
+
export type ApplicationContext = z.infer<typeof ApplicationContextSchema>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @careermate/workflows — definitions.ts
|
|
3
|
+
*
|
|
4
|
+
* Step-by-step workflows the AI should follow, encoded as DATA + helper text.
|
|
5
|
+
* Each step references concrete MCP tool names so the AI knows exactly which
|
|
6
|
+
* tool to call. Korean-first (read by the AI and surfaced to users).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface WorkflowDefinition {
|
|
10
|
+
/** Stable machine id. */
|
|
11
|
+
id: string;
|
|
12
|
+
/** Human-facing title (Korean). */
|
|
13
|
+
title: string;
|
|
14
|
+
/** One-line description of what this workflow accomplishes. */
|
|
15
|
+
description: string;
|
|
16
|
+
/** When the AI should start this workflow (natural-language trigger). */
|
|
17
|
+
trigger: string;
|
|
18
|
+
/** Ordered steps; each references the concrete MCP tool(s) to call. */
|
|
19
|
+
steps: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const WORKFLOWS: WorkflowDefinition[] = [
|
|
23
|
+
{
|
|
24
|
+
id: 'onboarding',
|
|
25
|
+
title: '온보딩 (첫 셋업)',
|
|
26
|
+
description:
|
|
27
|
+
'사용자가 처음 연결했을 때 프로필·이력서·자기소개서를 등록하고 대시보드를 여는 흐름.',
|
|
28
|
+
trigger:
|
|
29
|
+
'사용자가 처음 CareerMate에 연결했거나, "시작하자 / 셋업해줘 / 프로필 등록"이라고 요청할 때.',
|
|
30
|
+
steps: [
|
|
31
|
+
'`get_onboarding_status`를 호출해 현재 등록 상태(has_profile, has_resume, has_cover_letter, has_experience, has_skills, profile_completeness, next_steps)를 확인한다.',
|
|
32
|
+
'비어 있는 항목을 사용자에게 쉬운 한국어로 안내하고, 이력서/자기소개서 파일 업로드 또는 정보 입력을 요청한다.',
|
|
33
|
+
'수집한 정보를 구조화해 `save_profile`로 저장한다(이름·연락처·headline·summary·desired_roles·desired_conditions, 그리고 글쓰기 선호인 preferred_tone과 emphasis_points 포함). 파일 출처는 source=upload, 직접 입력은 manual.',
|
|
34
|
+
'업로드한 이력서/경력기술서/포트폴리오 본문을 `add_resume`로 저장한다(kind·title 지정, 대표 문서는 is_primary=true).',
|
|
35
|
+
'기존 자기소개서가 있으면 `add_cover_letter`로 저장한다(없으면 나중에 작성 가능함을 안내).',
|
|
36
|
+
'`open_dashboard`를 호출해 사용자가 자기 데이터를 확인하게 한다.',
|
|
37
|
+
'다시 `get_onboarding_status`로 완성도를 확인하고, 다음 단계(공고 분석 등)를 제안한다.',
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'analyze_job',
|
|
42
|
+
title: '공고 분석 (적합도 분석)',
|
|
43
|
+
description:
|
|
44
|
+
'채용 공고를 읽어 사용자 프로필과 비교하고, 공고와 적합도 분석을 저장한 뒤 자기소개서 작성을 제안하는 핵심 흐름.',
|
|
45
|
+
trigger:
|
|
46
|
+
'사용자가 채용 공고 링크나 본문을 붙여넣거나 "이 공고 분석해줘 / 나랑 잘 맞아?"라고 물을 때.',
|
|
47
|
+
steps: [
|
|
48
|
+
'공고 원문(붙여넣은 텍스트 또는 링크 내용)을 읽고 company, position, requirements, keywords, deadline 등 핵심 정보를 정리한다.',
|
|
49
|
+
'`get_application_context`를 호출해 한 번에 프로필·대표 이력서·경력·프로젝트·스킬·기존 자기소개서·최근 지원 현황·이전 적합도 분석·같은 회사/직무 관련 기록·글쓰기 선호(preferred_tone, emphasis_points)를 가져온다. (분석 대상 공고가 이미 저장돼 있으면 job_id도 함께 전달한다.)',
|
|
50
|
+
'공고 요구사항과 사용자 경력/스킬/프로젝트를 비교한다: 일치하는 강점(strengths)과 부족한 부분(gaps), 매칭 키워드(matched_keywords)와 누락 키워드(missing_keywords)를 도출하고, 종합 적합도 점수(score 0~100)와 요약(summary), 지원 전략 제안(recommendations)을 정리한다. 없는 경험을 지어내지 않는다.',
|
|
51
|
+
'아직 저장되지 않은 공고라면 `save_job_posting`으로 공고를 저장하고 job_id를 확보한다.',
|
|
52
|
+
'도출한 분석 결과를 `save_fit_analysis`로 저장한다(job_id, score, summary, strengths, gaps, matched_keywords, missing_keywords, recommendations).',
|
|
53
|
+
'분석 결과를 사용자에게 쉬운 한국어로 설명한다(잘 맞는 점, 보완할 점, 추천 전략). 기술 용어는 빼고 전달한다.',
|
|
54
|
+
'"이 공고에 맞춘 자기소개서를 써드릴까요?"라고 자기소개서 작성을 제안한다.',
|
|
55
|
+
'사용자가 동의하면 자기소개서를 작성하기 직전에 `get_writing_style_guide`를 호출해 "AI 티 안 나는 글쓰기 규칙"을 가져온다.',
|
|
56
|
+
'적합도 분석·글쓰기 선호와 글쓰기 규칙을 함께 반영해 자기소개서를 작성하고(번역투·클리셰·기계적 병렬 제거, 사실·수치는 그대로), `save_cover_letter_version`으로 저장한다(job_id 연결, note에 작성 의도 요약).',
|
|
57
|
+
'`open_application`으로 해당 지원 건을 열어 사용자가 결과를 확인하게 하고, 지원 상태 변경(예: 지원 예정/지원 완료)을 제안한다.',
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 'write_cover_letter',
|
|
62
|
+
title: '자기소개서 작성',
|
|
63
|
+
description:
|
|
64
|
+
'특정 공고에 맞춘 자기소개서를 사용자 확인을 거쳐 작성하고 버전으로 저장하는 흐름.',
|
|
65
|
+
trigger:
|
|
66
|
+
'사용자가 "자기소개서 써줘 / 이 공고용 자소서 만들어줘"라고 요청하거나, 공고 분석 후 작성 제안에 동의했을 때.',
|
|
67
|
+
steps: [
|
|
68
|
+
'`get_application_context`(가능하면 job_id 포함)를 호출해 프로필·경력·프로젝트·스킬·기존 자기소개서·이전 적합도 분석·글쓰기 선호(preferred_tone, emphasis_points)를 가져온다.',
|
|
69
|
+
'작성 전에 대상 공고, 강조할 포인트, 톤, 분량을 사용자에게 확인한다(확인 없이 큰 작업을 진행하지 않는다).',
|
|
70
|
+
'`get_writing_style_guide`를 호출해 "AI 티 안 나는 글쓰기 규칙"(번역투·클리셰·기계적 병렬·상투적 연결어·균일한 문장 리듬 제거)을 가져온다.',
|
|
71
|
+
'저장된 실제 경험·성과만으로 자기소개서를 작성하되, 위 글쓰기 규칙을 적용해 사람이 쓴 듯 자연스럽게 쓴다(사실·수치·고유명사는 그대로). 빈칸이나 확인이 필요한 부분은 추측하지 말고 표시해 사용자에게 묻는다.',
|
|
72
|
+
'완성본을 `save_cover_letter_version`으로 저장한다(기존 자기소개서면 cover_letter_id, 새로 만들면 title, 공고용이면 job_id, 변경 요약은 note, source=ai).',
|
|
73
|
+
'사용자가 원하면 `export_cover_letter`로 파일로 내보내고, 추가 수정 요청이 있으면 새 버전으로 다시 저장한다.',
|
|
74
|
+
'저장 결과를 쉬운 한국어로 알리고, 지원 상태 업데이트(`update_application_status`)나 면접 준비 등 다음 단계를 제안한다.',
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'manage_application_status',
|
|
79
|
+
title: '지원 상태 관리',
|
|
80
|
+
description:
|
|
81
|
+
'지원 진행 상황(8단계)을 업데이트하고 상태에 맞는 다음 행동을 제안하는 흐름.',
|
|
82
|
+
trigger:
|
|
83
|
+
'사용자가 "지원했어 / 서류 합격했어 / 면접 잡혔어 / 떨어졌어"처럼 진행 상황을 알리거나 상태 변경을 요청할 때.',
|
|
84
|
+
steps: [
|
|
85
|
+
'필요하면 `get_application_context` 또는 `get_job_posting`으로 대상 지원 건과 공고를 확인한다.',
|
|
86
|
+
'적절한 상태로 `update_application_status`를 호출한다. 8단계: draft(작성 중), planned(지원 예정), applied(지원 완료), document_passed(서류 합격), interview(면접 진행), final_passed(최종 합격), rejected(불합격), on_hold(보류). 변경 사유는 note에 남긴다.',
|
|
87
|
+
'변경 결과를 사용자에게 쉬운 한국어로 알린다.',
|
|
88
|
+
'상태에 맞는 다음 단계를 제안한다: applied → 결과 기다리기 안내, document_passed/interview → 면접 준비(prepare_interview) 제안, final_passed → 축하 및 마무리, rejected/on_hold → 회고 또는 다른 공고 탐색 제안.',
|
|
89
|
+
'필요하면 `open_application`으로 해당 지원 건을 열어 보여준다.',
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: 'prepare_interview',
|
|
94
|
+
title: '면접 준비',
|
|
95
|
+
description:
|
|
96
|
+
'서류 합격 이후 예상 질문·STAR 답변·1분 자기소개를 준비해 저장하는 흐름.',
|
|
97
|
+
trigger:
|
|
98
|
+
'지원 상태가 서류 합격(document_passed) 또는 면접 진행(interview)으로 바뀌었을 때, 또는 사용자가 "면접 준비 도와줘"라고 요청할 때.',
|
|
99
|
+
steps: [
|
|
100
|
+
'`get_application_context`(job_id 포함)를 호출해 공고, 프로필, 경력/프로젝트, 적합도 분석(강점·갭), 자기소개서를 가져온다.',
|
|
101
|
+
'공고와 사용자 경험을 바탕으로 예상 면접 질문(question, intent, followups, answer_outline)을 도출한다.',
|
|
102
|
+
'핵심 경험에 대한 STAR 가이드(question, situation, task, action, result)와 1분 자기소개(self_introduction) 초안을 작성한다. 실제 경험만 사용한다.',
|
|
103
|
+
'준비 내용을 `save_interview_prep`로 저장한다(job_id, questions, star_guides, self_introduction, notes).',
|
|
104
|
+
'준비 내용을 사용자에게 쉬운 한국어로 요약해 전달하고, 모의 면접/추가 질문 연습이나 상태 업데이트(`update_application_status`로 interview/final_passed)를 제안한다.',
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @careermate/workflows — public entry point.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the workflow data plus helpers to look up a single workflow and
|
|
5
|
+
* render it as readable Korean markdown (used in docs and MCP tool descriptions).
|
|
6
|
+
*/
|
|
7
|
+
export { WORKFLOWS } from './definitions.ts';
|
|
8
|
+
export type { WorkflowDefinition } from './definitions.ts';
|
|
9
|
+
|
|
10
|
+
import { WORKFLOWS, type WorkflowDefinition } from './definitions.ts';
|
|
11
|
+
|
|
12
|
+
/** Look up a single workflow by id. Returns undefined if the id is unknown. */
|
|
13
|
+
export function getWorkflow(id: string): WorkflowDefinition | undefined {
|
|
14
|
+
return WORKFLOWS.find((w) => w.id === id);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Render a workflow as a readable Korean markdown block. Used in documentation
|
|
19
|
+
* and as MCP tool descriptions so the AI can read the full step list inline.
|
|
20
|
+
* Returns undefined if the id is unknown.
|
|
21
|
+
*/
|
|
22
|
+
export function renderWorkflowMarkdown(id: string): string | undefined {
|
|
23
|
+
const wf = getWorkflow(id);
|
|
24
|
+
if (!wf) return undefined;
|
|
25
|
+
|
|
26
|
+
const lines: string[] = [];
|
|
27
|
+
lines.push(`# ${wf.title}`);
|
|
28
|
+
lines.push('');
|
|
29
|
+
lines.push(wf.description);
|
|
30
|
+
lines.push('');
|
|
31
|
+
lines.push(`**트리거:** ${wf.trigger}`);
|
|
32
|
+
lines.push('');
|
|
33
|
+
lines.push('## 단계');
|
|
34
|
+
wf.steps.forEach((step, i) => {
|
|
35
|
+
lines.push(`${i + 1}. ${step}`);
|
|
36
|
+
});
|
|
37
|
+
lines.push('');
|
|
38
|
+
return lines.join('\n');
|
|
39
|
+
}
|