careermate 0.0.2 → 0.0.4
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/INSTALL.md +3 -3
- package/README.md +10 -8
- package/bin/careermate.mjs +2 -2
- package/dist/doctor.mjs +185 -39
- package/dist/mcp.mjs +188 -42
- package/dist/migrate.mjs +23 -1
- package/dist/public/lib.js +5 -1
- package/dist/public/pages/applications.js +7 -0
- package/dist/public/pages/jobs.js +22 -15
- package/dist/public/styles.css +9 -0
- package/dist/site/index.html +3 -43
- package/dist/site/llms-install.txt +4 -6
- package/dist/site/start.html +122 -102
- package/dist/web.mjs +138 -13
- package/docs/MCP_TOOLS.md +22 -7
- package/docs/START_WORKFLOW.md +2 -1
- package/docs/WORKFLOWS.md +1 -1
- package/package.json +2 -2
- package/packages/core/src/services.ts +67 -5
- package/packages/db/src/repositories.ts +0 -0
- package/packages/mcp-tools/src/tools.ts +21 -18
- package/packages/prompts/src/onboarding.ts +1 -1
- package/packages/shared/src/enums.ts +7 -2
- package/packages/shared/src/errors.ts +33 -0
- package/packages/shared/src/index.ts +1 -0
- package/packages/shared/src/schemas.ts +56 -2
- package/packages/workflows/src/definitions.ts +1 -1
- package/scripts/init.ts +11 -5
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Korean because the primary audience is Korean job seekers; codes are stable
|
|
7
7
|
* machine values that must never change once persisted.
|
|
8
8
|
*/
|
|
9
|
+
import { conflict } from './errors.ts';
|
|
9
10
|
|
|
10
11
|
/** Application lifecycle. Drives the Applications board in the dashboard. */
|
|
11
12
|
export const APPLICATION_STATUSES = [
|
|
@@ -84,8 +85,12 @@ export function canTransitionStatus(from: ApplicationStatus, to: ApplicationStat
|
|
|
84
85
|
/** Throw a clear, user-facing (Korean) error when a status transition is not allowed. */
|
|
85
86
|
export function assertStatusTransition(from: ApplicationStatus, to: ApplicationStatus): void {
|
|
86
87
|
if (!canTransitionStatus(from, to)) {
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
const suffix =
|
|
89
|
+
to === 'draft'
|
|
90
|
+
? ` ('${APPLICATION_STATUS_LABELS[from]}' 단계의 지원은 작성 중(미추적)으로 되돌릴 수 없습니다. 초기 단계로 옮기려면 '지원 예정'을 선택하세요.)`
|
|
91
|
+
: '';
|
|
92
|
+
throw conflict(
|
|
93
|
+
`'${APPLICATION_STATUS_LABELS[from]}'에서 '${APPLICATION_STATUS_LABELS[to]}'(으)로는 상태를 바꿀 수 없습니다.${suffix}`,
|
|
89
94
|
);
|
|
90
95
|
}
|
|
91
96
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain errors whose message is written as user-facing Korean guidance.
|
|
3
|
+
*
|
|
4
|
+
* The split matters at the HTTP boundary: an *expected* domain failure (공고 없음,
|
|
5
|
+
* 금지된 상태 전환, 저장 게이트 차단 …) should reach the dashboard with its real
|
|
6
|
+
* message and an accurate status code, exactly like the MCP layer already relays
|
|
7
|
+
* `e.message` via `fail(...)`. An *unexpected* internal error (a SQLite fault, a
|
|
8
|
+
* bug) must stay an opaque 500 so we never leak internals. Plain `Error` can't be
|
|
9
|
+
* told apart from the latter, so user-facing failures throw `CareerMateError` and
|
|
10
|
+
* the HTTP layer maps only those to `httpStatus` + message.
|
|
11
|
+
*/
|
|
12
|
+
export class CareerMateError extends Error {
|
|
13
|
+
constructor(
|
|
14
|
+
message: string,
|
|
15
|
+
/** HTTP status the dashboard should return (404 not-found, 409 conflict, 400 validation). */
|
|
16
|
+
readonly httpStatus = 400,
|
|
17
|
+
/** Optional machine-readable code for the client. */
|
|
18
|
+
readonly code?: string,
|
|
19
|
+
) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = 'CareerMateError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The requested record does not exist (HTTP 404). */
|
|
26
|
+
export function notFound(message: string): CareerMateError {
|
|
27
|
+
return new CareerMateError(message, 404, 'not_found');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The action conflicts with the record's current lifecycle state (HTTP 409). */
|
|
31
|
+
export function conflict(message: string): CareerMateError {
|
|
32
|
+
return new CareerMateError(message, 409, 'conflict');
|
|
33
|
+
}
|
|
@@ -72,8 +72,32 @@ const reqBody = z
|
|
|
72
72
|
.string()
|
|
73
73
|
.max(MAX_BODY)
|
|
74
74
|
.refine((s) => s.trim().length > 0, { message: EMPTY_MSG });
|
|
75
|
-
/**
|
|
76
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Coerce a stray scalar into a string array. MCP/LLM clients routinely serialize
|
|
77
|
+
* array arguments as a *string* — a JSON-encoded array (`'["Java","Spring"]'`) or
|
|
78
|
+
* a comma/newline-separated list (`'Java, Spring'`) — instead of a real array. The
|
|
79
|
+
* SDK then rejects the whole tool call with "Expected array, received string", so
|
|
80
|
+
* a single mis-serialized field (keywords, requirements, strengths, tech, tags…)
|
|
81
|
+
* fails the entire save. Absorb those shapes here; real arrays and non-strings
|
|
82
|
+
* pass straight through. The emitted JSON schema still advertises `array`, so the
|
|
83
|
+
* client is still told the correct shape — this only rescues the cases it ignores.
|
|
84
|
+
*/
|
|
85
|
+
function toStrList(v: unknown): unknown {
|
|
86
|
+
if (typeof v !== 'string') return v;
|
|
87
|
+
const s = v.trim();
|
|
88
|
+
if (!s) return [];
|
|
89
|
+
if (s.startsWith('[')) {
|
|
90
|
+
try {
|
|
91
|
+
const parsed = JSON.parse(s);
|
|
92
|
+
if (Array.isArray(parsed)) return parsed;
|
|
93
|
+
} catch {
|
|
94
|
+
/* not valid JSON — fall through to delimiter split */
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return s.split(/[\n,]/).map((x) => x.trim()).filter(Boolean);
|
|
98
|
+
}
|
|
99
|
+
/** Bounded array of bounded strings; tolerant of stringified inputs (see toStrList). */
|
|
100
|
+
const strList = z.preprocess(toStrList, z.array(z.string().max(MAX_ITEM)).max(MAX_ITEMS));
|
|
77
101
|
|
|
78
102
|
const baseRecord = {
|
|
79
103
|
id,
|
|
@@ -151,6 +175,16 @@ export const ExperienceRecordSchema = z.object({
|
|
|
151
175
|
});
|
|
152
176
|
export type ExperienceRecord = z.infer<typeof ExperienceRecordSchema>;
|
|
153
177
|
|
|
178
|
+
/** Batch input: add several experiences in one MCP call (one item → array of one). */
|
|
179
|
+
export const ExperienceBatchInputSchema = z.object({
|
|
180
|
+
experiences: z
|
|
181
|
+
.array(ExperienceInputSchema)
|
|
182
|
+
.min(1)
|
|
183
|
+
.max(MAX_ITEMS)
|
|
184
|
+
.describe('추가할 경력 목록(한 건이어도 배열로). 같은 회사·직무·입사일은 갱신되고 중복 생성되지 않습니다.'),
|
|
185
|
+
});
|
|
186
|
+
export type ExperienceBatchInput = z.infer<typeof ExperienceBatchInputSchema>;
|
|
187
|
+
|
|
154
188
|
/* ------------------------------------------------------------------ Project */
|
|
155
189
|
|
|
156
190
|
export const ProjectInputSchema = z.object({
|
|
@@ -180,6 +214,16 @@ export const ProjectRecordSchema = z.object({
|
|
|
180
214
|
});
|
|
181
215
|
export type ProjectRecord = z.infer<typeof ProjectRecordSchema>;
|
|
182
216
|
|
|
217
|
+
/** Batch input: add several projects in one MCP call (one item → array of one). */
|
|
218
|
+
export const ProjectBatchInputSchema = z.object({
|
|
219
|
+
projects: z
|
|
220
|
+
.array(ProjectInputSchema)
|
|
221
|
+
.min(1)
|
|
222
|
+
.max(MAX_ITEMS)
|
|
223
|
+
.describe('추가할 프로젝트 목록(한 개여도 배열로). 같은 이름의 프로젝트는 갱신되고 중복 생성되지 않습니다.'),
|
|
224
|
+
});
|
|
225
|
+
export type ProjectBatchInput = z.infer<typeof ProjectBatchInputSchema>;
|
|
226
|
+
|
|
183
227
|
/* -------------------------------------------------------------------- Skill */
|
|
184
228
|
|
|
185
229
|
export const SkillInputSchema = z.object({
|
|
@@ -201,6 +245,16 @@ export const SkillRecordSchema = z.object({
|
|
|
201
245
|
});
|
|
202
246
|
export type SkillRecord = z.infer<typeof SkillRecordSchema>;
|
|
203
247
|
|
|
248
|
+
/** Batch input: add several skills in one MCP call (one item → array of one). */
|
|
249
|
+
export const SkillBatchInputSchema = z.object({
|
|
250
|
+
skills: z
|
|
251
|
+
.array(SkillInputSchema)
|
|
252
|
+
.min(1)
|
|
253
|
+
.max(MAX_ITEMS)
|
|
254
|
+
.describe('추가할 기술스택 목록(한 개여도 배열로). 같은 이름의 기술은 갱신되고 중복 생성되지 않습니다.'),
|
|
255
|
+
});
|
|
256
|
+
export type SkillBatchInput = z.infer<typeof SkillBatchInputSchema>;
|
|
257
|
+
|
|
204
258
|
/* ----------------------------------------------------------------- Document */
|
|
205
259
|
|
|
206
260
|
export const DocumentInputSchema = z.object({
|
|
@@ -33,7 +33,7 @@ export const WORKFLOWS: WorkflowDefinition[] = [
|
|
|
33
33
|
'파일 입력 분기: ① 사용자가 "파일이 있다"고 하면 `open_inbox`로 인입 폴더를 열어 파일을 넣게 하고, "다 넣었다"고 하면 `read_inbox`로 본문을 읽는다(pdf·이미지는 read_inbox가 경로만 주므로 클라이언트의 파일 읽기로 직접 읽는다). ② 사용자가 파일 경로를 직접 알려주면 `read_document`로 읽는다. ③ "없다/직접 입력/텍스트로 붙여넣기"면 폴더를 열지 말고 받은 텍스트를 그대로 쓴다. (묻지도 않고 open_inbox로 폴더를 자동으로 열지 않는다.)',
|
|
34
34
|
'수집한 정보를 구조화해 `save_profile`로 저장한다(이름·연락처·headline·summary·desired_roles·desired_conditions, 그리고 글쓰기 선호인 preferred_tone과 emphasis_points 포함). 파일 출처는 source=upload, 직접 입력은 manual.',
|
|
35
35
|
'업로드한 이력서/경력기술서/포트폴리오 본문을 `add_resume`로 저장한다(kind·title 지정, 대표 문서는 is_primary=true).',
|
|
36
|
-
'경력·프로젝트·스킬이 파악되면 `add_experience`·`add_project`·`add_skill`로
|
|
36
|
+
'경력·프로젝트·스킬이 파악되면 `add_experience`·`add_project`·`add_skill`로 저장한다 — 각 도구는 배열을 받으므로 파악한 항목을 한 번에 모아 넘긴다(항목마다 반복 호출하지 않는다). 같은 항목을 다시 넣어도 중복 없이 갱신된다(완성도의 has_experience/has_skills에 반영).',
|
|
37
37
|
'기존 자기소개서가 있으면 `save_cover_letter_version`으로 저장한다(title+content면 v1로 저장, 없으면 나중에 작성 가능함을 안내).',
|
|
38
38
|
'`open_dashboard`를 호출해 사용자가 자기 데이터를 확인하게 한다.',
|
|
39
39
|
'다시 `get_onboarding_status`로 완성도를 확인하고, 다음 단계(공고 분석 등)를 제안한다.',
|
package/scripts/init.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* v1이 지원하는 연결 대상(우선순위 순):
|
|
9
9
|
* - Claude Desktop → claude_desktop_config.json (JSON · mcpServers)
|
|
10
|
-
* - Claude Code →
|
|
10
|
+
* - Claude Code → <현재 작업 폴더>/.mcp.json (JSON · mcpServers, project scope)
|
|
11
11
|
* - Codex CLI → ~/.codex/config.toml (TOML · [mcp_servers.careermate])
|
|
12
12
|
* - Cursor → ~/.cursor/mcp.json (JSON · mcpServers, 기타 MCP 클라이언트 호환)
|
|
13
13
|
*
|
|
@@ -80,9 +80,15 @@ function cursorConfigPath(): string {
|
|
|
80
80
|
return path.join(os.homedir(), '.cursor', 'mcp.json');
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* Claude Code project scope 설정: 사용자가 지금 작업 중인 폴더에 둔다.
|
|
85
|
+
*
|
|
86
|
+
* `npx -y careermate init`은 패키지를 임시 캐시에 풀어 실행하므로 ROOT는 사용자의
|
|
87
|
+
* 프로젝트가 아니라 설치물 위치다. Claude Code는 실제로 열 폴더의 `.mcp.json`만
|
|
88
|
+
* 승인/로드하므로 cwd를 프로젝트 루트로 삼는다.
|
|
89
|
+
*/
|
|
84
90
|
function claudeCodeConfigPath(): string {
|
|
85
|
-
return path.join(
|
|
91
|
+
return path.join(process.cwd(), '.mcp.json');
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
/** Codex CLI 글로벌 설정(CODEX_HOME 우선). */
|
|
@@ -306,7 +312,7 @@ function printConfigs(server: ServerEntry): void {
|
|
|
306
312
|
2,
|
|
307
313
|
),
|
|
308
314
|
);
|
|
309
|
-
console.log('\n · Claude Code는 위 내용을
|
|
315
|
+
console.log('\n · Claude Code는 위 내용을 실제 작업 폴더의 .mcp.json 으로 저장하면 됩니다.');
|
|
310
316
|
console.log(' · Gemini CLI는 ~/.gemini/settings.json, Antigravity는 ~/.gemini/(config|antigravity|antigravity-cli)/mcp_config.json 의 mcpServers에 같은 블록을 넣으세요(type 키는 없어도 됨).');
|
|
311
317
|
console.log(` · 또는 명령으로: ${shellJoin(['claude', 'mcp', 'add', '--scope', 'project', '--transport', 'stdio', PKG, '--', ...cmd])}`);
|
|
312
318
|
|
|
@@ -362,7 +368,7 @@ function main(): void {
|
|
|
362
368
|
console.log('\n✅ 거의 끝났습니다. 다음만 해주세요:');
|
|
363
369
|
console.log(' 1) 연결한 AI 클라이언트를 완전히 종료했다가 다시 켜기');
|
|
364
370
|
if (connectedIds.has('claude-code')) {
|
|
365
|
-
console.log(' (Claude Code는
|
|
371
|
+
console.log(' (Claude Code는 이 명령을 실행한 폴더의 .mcp.json을 처음 띄울 때 1회 "승인"을 물어봅니다 — 승인해 주세요.)');
|
|
366
372
|
}
|
|
367
373
|
console.log(' 2) AI에게: "get_onboarding_status 호출해서 연결됐는지 확인해줘"');
|
|
368
374
|
console.log(' 3) 내 데이터 대시보드 열기: careermate start');
|