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
package/dist/web.mjs
CHANGED
|
@@ -39766,6 +39766,24 @@ function setVerifyStrict(on) {
|
|
|
39766
39766
|
return on;
|
|
39767
39767
|
}
|
|
39768
39768
|
|
|
39769
|
+
// packages/shared/src/errors.ts
|
|
39770
|
+
var CareerMateError = class extends Error {
|
|
39771
|
+
constructor(message, httpStatus = 400, code) {
|
|
39772
|
+
super(message);
|
|
39773
|
+
this.httpStatus = httpStatus;
|
|
39774
|
+
this.code = code;
|
|
39775
|
+
this.name = "CareerMateError";
|
|
39776
|
+
}
|
|
39777
|
+
httpStatus;
|
|
39778
|
+
code;
|
|
39779
|
+
};
|
|
39780
|
+
function notFound(message) {
|
|
39781
|
+
return new CareerMateError(message, 404, "not_found");
|
|
39782
|
+
}
|
|
39783
|
+
function conflict(message) {
|
|
39784
|
+
return new CareerMateError(message, 409, "conflict");
|
|
39785
|
+
}
|
|
39786
|
+
|
|
39769
39787
|
// packages/shared/src/enums.ts
|
|
39770
39788
|
var APPLICATION_STATUSES = [
|
|
39771
39789
|
"draft",
|
|
@@ -39826,8 +39844,9 @@ function canTransitionStatus(from, to) {
|
|
|
39826
39844
|
}
|
|
39827
39845
|
function assertStatusTransition(from, to) {
|
|
39828
39846
|
if (!canTransitionStatus(from, to)) {
|
|
39829
|
-
|
|
39830
|
-
|
|
39847
|
+
const suffix = to === "draft" ? ` ('${APPLICATION_STATUS_LABELS[from]}' \uB2E8\uACC4\uC758 \uC9C0\uC6D0\uC740 \uC791\uC131 \uC911(\uBBF8\uCD94\uC801)\uC73C\uB85C \uB418\uB3CC\uB9B4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uCD08\uAE30 \uB2E8\uACC4\uB85C \uC62E\uAE30\uB824\uBA74 '\uC9C0\uC6D0 \uC608\uC815'\uC744 \uC120\uD0DD\uD558\uC138\uC694.)` : "";
|
|
39848
|
+
throw conflict(
|
|
39849
|
+
`'${APPLICATION_STATUS_LABELS[from]}'\uC5D0\uC11C '${APPLICATION_STATUS_LABELS[to]}'(\uC73C)\uB85C\uB294 \uC0C1\uD0DC\uB97C \uBC14\uAFC0 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.${suffix}`
|
|
39831
39850
|
);
|
|
39832
39851
|
}
|
|
39833
39852
|
}
|
|
@@ -43926,7 +43945,20 @@ var optLine = external_exports.string().max(MAX_LINE).optional();
|
|
|
43926
43945
|
var optNote = external_exports.string().max(MAX_NOTE).optional();
|
|
43927
43946
|
var optBody = external_exports.string().max(MAX_BODY).optional();
|
|
43928
43947
|
var reqBody = external_exports.string().max(MAX_BODY).refine((s) => s.trim().length > 0, { message: EMPTY_MSG });
|
|
43929
|
-
|
|
43948
|
+
function toStrList(v) {
|
|
43949
|
+
if (typeof v !== "string") return v;
|
|
43950
|
+
const s = v.trim();
|
|
43951
|
+
if (!s) return [];
|
|
43952
|
+
if (s.startsWith("[")) {
|
|
43953
|
+
try {
|
|
43954
|
+
const parsed = JSON.parse(s);
|
|
43955
|
+
if (Array.isArray(parsed)) return parsed;
|
|
43956
|
+
} catch {
|
|
43957
|
+
}
|
|
43958
|
+
}
|
|
43959
|
+
return s.split(/[\n,]/).map((x) => x.trim()).filter(Boolean);
|
|
43960
|
+
}
|
|
43961
|
+
var strList = external_exports.preprocess(toStrList, external_exports.array(external_exports.string().max(MAX_ITEM)).max(MAX_ITEMS));
|
|
43930
43962
|
var baseRecord = {
|
|
43931
43963
|
id,
|
|
43932
43964
|
created_at: isoDateTime,
|
|
@@ -43988,6 +44020,9 @@ var ExperienceRecordSchema = external_exports.object({
|
|
|
43988
44020
|
tech: external_exports.array(external_exports.string()),
|
|
43989
44021
|
order_index: external_exports.number()
|
|
43990
44022
|
});
|
|
44023
|
+
var ExperienceBatchInputSchema = external_exports.object({
|
|
44024
|
+
experiences: external_exports.array(ExperienceInputSchema).min(1).max(MAX_ITEMS).describe("\uCD94\uAC00\uD560 \uACBD\uB825 \uBAA9\uB85D(\uD55C \uAC74\uC774\uC5B4\uB3C4 \uBC30\uC5F4\uB85C). \uAC19\uC740 \uD68C\uC0AC\xB7\uC9C1\uBB34\xB7\uC785\uC0AC\uC77C\uC740 \uAC31\uC2E0\uB418\uACE0 \uC911\uBCF5 \uC0DD\uC131\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.")
|
|
44025
|
+
});
|
|
43991
44026
|
var ProjectInputSchema = external_exports.object({
|
|
43992
44027
|
name: reqLine,
|
|
43993
44028
|
role: optLine,
|
|
@@ -44011,6 +44046,9 @@ var ProjectRecordSchema = external_exports.object({
|
|
|
44011
44046
|
end_date: external_exports.string().nullable(),
|
|
44012
44047
|
order_index: external_exports.number()
|
|
44013
44048
|
});
|
|
44049
|
+
var ProjectBatchInputSchema = external_exports.object({
|
|
44050
|
+
projects: external_exports.array(ProjectInputSchema).min(1).max(MAX_ITEMS).describe("\uCD94\uAC00\uD560 \uD504\uB85C\uC81D\uD2B8 \uBAA9\uB85D(\uD55C \uAC1C\uC5EC\uB3C4 \uBC30\uC5F4\uB85C). \uAC19\uC740 \uC774\uB984\uC758 \uD504\uB85C\uC81D\uD2B8\uB294 \uAC31\uC2E0\uB418\uACE0 \uC911\uBCF5 \uC0DD\uC131\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.")
|
|
44051
|
+
});
|
|
44014
44052
|
var SkillInputSchema = external_exports.object({
|
|
44015
44053
|
name: reqLine,
|
|
44016
44054
|
category: external_exports.string().max(MAX_LINE).optional().describe("\uC5B8\uC5B4/\uD504\uB808\uC784\uC6CC\uD06C/\uD234/\uC18C\uD504\uD2B8\uC2A4\uD0AC \uB4F1"),
|
|
@@ -44026,6 +44064,9 @@ var SkillRecordSchema = external_exports.object({
|
|
|
44026
44064
|
years: external_exports.number().nullable(),
|
|
44027
44065
|
order_index: external_exports.number()
|
|
44028
44066
|
});
|
|
44067
|
+
var SkillBatchInputSchema = external_exports.object({
|
|
44068
|
+
skills: external_exports.array(SkillInputSchema).min(1).max(MAX_ITEMS).describe("\uCD94\uAC00\uD560 \uAE30\uC220\uC2A4\uD0DD \uBAA9\uB85D(\uD55C \uAC1C\uC5EC\uB3C4 \uBC30\uC5F4\uB85C). \uAC19\uC740 \uC774\uB984\uC758 \uAE30\uC220\uC740 \uAC31\uC2E0\uB418\uACE0 \uC911\uBCF5 \uC0DD\uC131\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.")
|
|
44069
|
+
});
|
|
44029
44070
|
var DocumentInputSchema = external_exports.object({
|
|
44030
44071
|
kind: external_exports.enum(DOCUMENT_KINDS),
|
|
44031
44072
|
title: reqLine,
|
|
@@ -44222,7 +44263,7 @@ var ApplicationContextSchema = external_exports.object({
|
|
|
44222
44263
|
});
|
|
44223
44264
|
|
|
44224
44265
|
// packages/shared/src/build-info.ts
|
|
44225
|
-
var APP_VERSION = true ? "0.0.
|
|
44266
|
+
var APP_VERSION = true ? "0.0.4" : "0.0.0-dev";
|
|
44226
44267
|
var BUNDLED = true;
|
|
44227
44268
|
|
|
44228
44269
|
// packages/shared/src/index.ts
|
|
@@ -44237,6 +44278,7 @@ function now() {
|
|
|
44237
44278
|
|
|
44238
44279
|
// packages/db/src/repositories.ts
|
|
44239
44280
|
var PROFILE_ID = "profile-singleton";
|
|
44281
|
+
var norm = (s) => (s ?? "").trim().toLowerCase();
|
|
44240
44282
|
function mapProfile(r) {
|
|
44241
44283
|
return {
|
|
44242
44284
|
id: r.id,
|
|
@@ -44314,6 +44356,9 @@ function mapExperience(r) {
|
|
|
44314
44356
|
updated_at: r.updated_at
|
|
44315
44357
|
};
|
|
44316
44358
|
}
|
|
44359
|
+
function expKey(company, role, start) {
|
|
44360
|
+
return [norm(company), norm(role), norm(start)].join("\0");
|
|
44361
|
+
}
|
|
44317
44362
|
var experienceRepo = {
|
|
44318
44363
|
list() {
|
|
44319
44364
|
return getDb().prepare(`SELECT * FROM experiences ORDER BY order_index ASC, start_date DESC`).all().map(mapExperience);
|
|
@@ -44322,6 +44367,32 @@ var experienceRepo = {
|
|
|
44322
44367
|
const r = getDb().prepare(`SELECT * FROM experiences WHERE id = ?`).get(id3);
|
|
44323
44368
|
return r ? mapExperience(r) : null;
|
|
44324
44369
|
},
|
|
44370
|
+
/** Existing experience with the same company+role+start_date, or null (for idempotent batch upserts). */
|
|
44371
|
+
findMatch(input) {
|
|
44372
|
+
const key = expKey(input.company, input.role ?? null, input.start_date ?? null);
|
|
44373
|
+
return this.list().find((e) => expKey(e.company, e.role, e.start_date) === key) ?? null;
|
|
44374
|
+
},
|
|
44375
|
+
/** Insert, or merge into the matching experience (omitted fields keep their stored value). */
|
|
44376
|
+
upsert(input) {
|
|
44377
|
+
const existing = this.findMatch(input);
|
|
44378
|
+
if (existing) return { record: this.update(existing.id, input), created: false };
|
|
44379
|
+
return { record: this.add(input), created: true };
|
|
44380
|
+
},
|
|
44381
|
+
/** Batch insert/merge in one transaction; within-batch duplicates collapse onto one row. */
|
|
44382
|
+
addMany(inputs) {
|
|
44383
|
+
return tx(() => {
|
|
44384
|
+
const records = [];
|
|
44385
|
+
let created = 0;
|
|
44386
|
+
let updated = 0;
|
|
44387
|
+
for (const input of inputs) {
|
|
44388
|
+
const { record, created: isNew } = this.upsert(input);
|
|
44389
|
+
records.push(record);
|
|
44390
|
+
if (isNew) created += 1;
|
|
44391
|
+
else updated += 1;
|
|
44392
|
+
}
|
|
44393
|
+
return { records, created, updated };
|
|
44394
|
+
});
|
|
44395
|
+
},
|
|
44325
44396
|
add(input) {
|
|
44326
44397
|
const ts = now();
|
|
44327
44398
|
const id3 = newId("exp_");
|
|
@@ -44395,6 +44466,32 @@ var projectRepo = {
|
|
|
44395
44466
|
const r = getDb().prepare(`SELECT * FROM projects WHERE id = ?`).get(id3);
|
|
44396
44467
|
return r ? mapProject(r) : null;
|
|
44397
44468
|
},
|
|
44469
|
+
/** Existing project with the same name (case-insensitive), or null (for idempotent batch upserts). */
|
|
44470
|
+
findByName(name) {
|
|
44471
|
+
const key = norm(name);
|
|
44472
|
+
return this.list().find((p) => norm(p.name) === key) ?? null;
|
|
44473
|
+
},
|
|
44474
|
+
/** Insert, or merge into the same-named project (omitted fields keep their stored value). */
|
|
44475
|
+
upsert(input) {
|
|
44476
|
+
const existing = this.findByName(input.name);
|
|
44477
|
+
if (existing) return { record: this.update(existing.id, input), created: false };
|
|
44478
|
+
return { record: this.add(input), created: true };
|
|
44479
|
+
},
|
|
44480
|
+
/** Batch insert/merge in one transaction; within-batch duplicates collapse onto one row. */
|
|
44481
|
+
addMany(inputs) {
|
|
44482
|
+
return tx(() => {
|
|
44483
|
+
const records = [];
|
|
44484
|
+
let created = 0;
|
|
44485
|
+
let updated = 0;
|
|
44486
|
+
for (const input of inputs) {
|
|
44487
|
+
const { record, created: isNew } = this.upsert(input);
|
|
44488
|
+
records.push(record);
|
|
44489
|
+
if (isNew) created += 1;
|
|
44490
|
+
else updated += 1;
|
|
44491
|
+
}
|
|
44492
|
+
return { records, created, updated };
|
|
44493
|
+
});
|
|
44494
|
+
},
|
|
44398
44495
|
add(input) {
|
|
44399
44496
|
const ts = now();
|
|
44400
44497
|
const id3 = newId("prj_");
|
|
@@ -44462,6 +44559,32 @@ var skillRepo = {
|
|
|
44462
44559
|
const r = getDb().prepare(`SELECT * FROM skills WHERE id = ?`).get(id3);
|
|
44463
44560
|
return r ? mapSkill(r) : null;
|
|
44464
44561
|
},
|
|
44562
|
+
/** Existing skill with the same name (case-insensitive), or null (for idempotent batch upserts). */
|
|
44563
|
+
findByName(name) {
|
|
44564
|
+
const key = norm(name);
|
|
44565
|
+
return this.list().find((s) => norm(s.name) === key) ?? null;
|
|
44566
|
+
},
|
|
44567
|
+
/** Insert, or merge into the same-named skill (omitted fields keep their stored value). */
|
|
44568
|
+
upsert(input) {
|
|
44569
|
+
const existing = this.findByName(input.name);
|
|
44570
|
+
if (existing) return { record: this.update(existing.id, input), created: false };
|
|
44571
|
+
return { record: this.add(input), created: true };
|
|
44572
|
+
},
|
|
44573
|
+
/** Batch insert/merge in one transaction; within-batch duplicates collapse onto one row. */
|
|
44574
|
+
addMany(inputs) {
|
|
44575
|
+
return tx(() => {
|
|
44576
|
+
const records = [];
|
|
44577
|
+
let created = 0;
|
|
44578
|
+
let updated = 0;
|
|
44579
|
+
for (const input of inputs) {
|
|
44580
|
+
const { record, created: isNew } = this.upsert(input);
|
|
44581
|
+
records.push(record);
|
|
44582
|
+
if (isNew) created += 1;
|
|
44583
|
+
else updated += 1;
|
|
44584
|
+
}
|
|
44585
|
+
return { records, created, updated };
|
|
44586
|
+
});
|
|
44587
|
+
},
|
|
44465
44588
|
add(input) {
|
|
44466
44589
|
const ts = now();
|
|
44467
44590
|
const id3 = newId("skl_");
|
|
@@ -45536,7 +45659,7 @@ function styleSignals(text) {
|
|
|
45536
45659
|
}
|
|
45537
45660
|
var DISCLAIMER = "\uC774 \uC810\uAC80\uC740 LLM \uC5C6\uC774 \uC140 \uC218 \uC788\uB294 \uAC83\uB9CC \uBD05\uB2C8\uB2E4(\uC22B\uC790 \uCD9C\uCC98\xB7\uBB38\uCCB4 \uC2E0\uD638). \uD1B5\uACFC=\uD569\uACA9\uC774 \uC544\uB2C8\uBA70, \uC0AC\uC2E4\xB7\uB3D9\uBB38\uC11C\uB2F5\xB7\uC758\uBBF8 \uD310\uB2E8\uC740 \uB2F9\uC2E0(AI)\uC774 \uC9C1\uC811 \uD574\uC57C \uD569\uB2C8\uB2E4.";
|
|
45538
45661
|
function lintArtifact(kind, text, corpus, opts = {}) {
|
|
45539
|
-
const
|
|
45662
|
+
const norm2 = normalizeText(text);
|
|
45540
45663
|
const corpusAvailable = (corpus.documents + corpus.structured).trim().length > 0;
|
|
45541
45664
|
const provenance = analyzeProvenance(text, corpus);
|
|
45542
45665
|
const signals = styleSignals(text);
|
|
@@ -45560,7 +45683,7 @@ function lintArtifact(kind, text, corpus, opts = {}) {
|
|
|
45560
45683
|
}
|
|
45561
45684
|
return {
|
|
45562
45685
|
kind,
|
|
45563
|
-
charCount: [...
|
|
45686
|
+
charCount: [...norm2].length,
|
|
45564
45687
|
signals,
|
|
45565
45688
|
provenance,
|
|
45566
45689
|
blocking,
|
|
@@ -45623,7 +45746,7 @@ function saveJobPosting(input, id3) {
|
|
|
45623
45746
|
}
|
|
45624
45747
|
function saveFitAnalysis(input) {
|
|
45625
45748
|
const job = jobRepo.get(input.job_id);
|
|
45626
|
-
if (!job) throw
|
|
45749
|
+
if (!job) throw notFound(`\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.job_id}. \uBA3C\uC800 save_job_posting\uC73C\uB85C \uACF5\uACE0\uB97C \uC800\uC7A5\uD558\uC138\uC694.`);
|
|
45627
45750
|
applicationRepo.ensure(job.id, "draft");
|
|
45628
45751
|
const fit = fitRepo.save(input);
|
|
45629
45752
|
const scoreText = fit.score != null ? ` (\uC801\uD569\uB3C4 ${fit.score}\uC810)` : "";
|
|
@@ -45638,7 +45761,7 @@ function saveCoverLetterVersion(input) {
|
|
|
45638
45761
|
const detail = verification.blocking.map((b) => `${b.label}: ${b.detail}`).join("; ");
|
|
45639
45762
|
const strictHit = verification.blocking.some((b) => b.id === "unverified_numbers_strict");
|
|
45640
45763
|
const tail = strictHit ? `\uC5C4\uACA9 \uBAA8\uB4DC\uAC00 \uCF1C\uC838 \uC788\uC5B4, \uC62C\uB9B0 \uC774\uB825\uC11C \uBCF8\uBB38\uC5D0 \uC5C6\uB294 \uC218\uCE58\uB294 \uB9C9\uD799\uB2C8\uB2E4. \uC774\uB825\uC11C\uB97C \uC62C\uB9AC\uAC70\uB098 \uC5C4\uACA9 \uBAA8\uB4DC\uB97C \uB044\uAC70\uB098, \uC758\uB3C4\uD55C \uAC12\uC774\uBA74 force: true\uB85C \uC800\uC7A5\uD558\uC138\uC694.` : `\uC774 \uC218\uCE58\uB294 \uC800\uC7A5\uB41C \uACBD\uB825\xB7\uC774\uB825\uC11C\xB7\uD504\uB85C\uC81D\uD2B8\uC5D0 \uC5C6\uC2B5\uB2C8\uB2E4. \uC6D0\uBCF8 \uB370\uC774\uD130\uC758 \uC2E4\uC81C \uC218\uCE58\uB85C \uACE0\uCE58\uAC70\uB098, \uC758\uB3C4\uD55C \uAC12\uC774 \uB9DE\uB2E4\uBA74 \uBA3C\uC800 \uACBD\uB825/\uD504\uB85C\uC81D\uD2B8\uC5D0 \uADF8 \uC218\uCE58\uB97C \uCD94\uAC00\uD55C \uB4A4 \uB2E4\uC2DC \uC800\uC7A5\uD558\uC138\uC694(\uADF8\uB300\uB85C \uC800\uC7A5\uD558\uB824\uBA74 force: true).`;
|
|
45641
|
-
throw new
|
|
45764
|
+
throw new CareerMateError(`\uC800\uC7A5 \uC804 \uC790\uB3D9 \uC810\uAC80\uC5D0\uC11C \uB9C9\uD614\uC2B5\uB2C8\uB2E4 \u2014 ${detail}. ${tail}`, 400, "save_gate");
|
|
45642
45765
|
}
|
|
45643
45766
|
const { coverLetter, version } = coverLetterRepo.addVersion({
|
|
45644
45767
|
cover_letter_id: input.cover_letter_id,
|
|
@@ -45665,7 +45788,7 @@ function saveCoverLetterVersion(input) {
|
|
|
45665
45788
|
}
|
|
45666
45789
|
function updateApplicationStatus(jobId, status, note) {
|
|
45667
45790
|
const job = jobRepo.get(jobId);
|
|
45668
|
-
if (!job) throw
|
|
45791
|
+
if (!job) throw notFound(`\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${jobId}`);
|
|
45669
45792
|
const application = applicationRepo.setStatus(jobId, status, note);
|
|
45670
45793
|
activityRepo.log(
|
|
45671
45794
|
"application_status_changed",
|
|
@@ -45680,11 +45803,11 @@ function updateApplicationStatus(jobId, status, note) {
|
|
|
45680
45803
|
}
|
|
45681
45804
|
function saveInterviewPrep(input) {
|
|
45682
45805
|
const job = jobRepo.get(input.job_id);
|
|
45683
|
-
if (!job) throw
|
|
45806
|
+
if (!job) throw notFound(`\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.job_id}`);
|
|
45684
45807
|
const app = applicationRepo.getByJob(job.id);
|
|
45685
45808
|
if (!app || !INTERVIEW_UNLOCK_STATUSES.includes(app.status)) {
|
|
45686
45809
|
const current = app ? APPLICATION_STATUS_LABELS[app.status] : "\uBBF8\uC9C0\uC6D0";
|
|
45687
|
-
throw
|
|
45810
|
+
throw conflict(
|
|
45688
45811
|
`\uBA74\uC811 \uC900\uBE44\uB294 '\uC11C\uB958 \uD569\uACA9' \uC774\uC0C1 \uC0C1\uD0DC\uC5D0\uC11C\uB9CC \uC800\uC7A5\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uD604\uC7AC \uC0C1\uD0DC: '${current}'. \uBA3C\uC800 update_application_status\uB85C \uC0C1\uD0DC\uB97C \uC62C\uB9B0 \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.`
|
|
45689
45812
|
);
|
|
45690
45813
|
}
|
|
@@ -45838,7 +45961,7 @@ var ONBOARDING_PROMPT = `\uC774\uC81C CareerMate\uC5D0 \uC5F0\uACB0\uB418\uC5C8\
|
|
|
45838
45961
|
- \uC774\uB984, \uC5F0\uB77D\uCC98, \uD55C \uC904 \uC18C\uAC1C(headline), \uC790\uAE30\uC18C\uAC1C \uC694\uC57D(summary), \uD76C\uB9DD \uC9C1\uBB34(desired_roles), \uD76C\uB9DD \uADFC\uBB34 \uC870\uAC74(desired_conditions)\uC744 \uC815\uB9AC\uD569\uB2C8\uB2E4.
|
|
45839
45962
|
- \uAE00\uC4F0\uAE30 \uC120\uD638\uB3C4 \uD568\uAED8 \uBB3C\uC5B4\uBCF4\uC138\uC694: \uC120\uD638 \uBB38\uCCB4(preferred_tone, \uC608: "\uB2F4\uBC31\uD558\uACE0 \uAD6C\uCCB4\uC801")\uC640 \uAC15\uC870\uD558\uACE0 \uC2F6\uC740 \uD575\uC2EC \uD3EC\uC778\uD2B8(emphasis_points). \uC774 \uC815\uBCF4\uB294 \uC774\uD6C4 \uC790\uAE30\uC18C\uAC1C\uC11C \uC791\uC131\uC5D0 \uADF8\uB300\uB85C \uD65C\uC6A9\uB429\uB2C8\uB2E4.
|
|
45840
45963
|
- \uC815\uB9AC\uD55C \uB0B4\uC6A9\uC744 \`save_profile\`\uB85C \uC800\uC7A5\uD558\uC138\uC694. \uD30C\uC77C\uC5D0\uC11C \uC77D\uC5B4\uC628 \uC815\uBCF4\uB294 source\uB97C 'upload', \uC0AC\uC6A9\uC790\uAC00 \uB9D0\uB85C \uC54C\uB824\uC900 \uC815\uBCF4\uB294 'manual'\uB85C \uD45C\uC2DC\uD558\uC138\uC694.
|
|
45841
|
-
- \uACBD\uB825\xB7\uD504\uB85C\uC81D\uD2B8\xB7\uC2A4\uD0AC \uC815\uBCF4\uAC00 \uC788\uC73C\uBA74 \`add_experience\`\xB7\`add_project\`\xB7\`add_skill\`\uB85C \
|
|
45964
|
+
- \uACBD\uB825\xB7\uD504\uB85C\uC81D\uD2B8\xB7\uC2A4\uD0AC \uC815\uBCF4\uAC00 \uC788\uC73C\uBA74 \`add_experience\`\xB7\`add_project\`\xB7\`add_skill\`\uB85C \uC800\uC7A5\uD558\uC138\uC694. \uAC01 \uB3C4\uAD6C\uB294 \uBC30\uC5F4\uC744 \uBC1B\uC73C\uB2C8 \uD30C\uC545\uD55C \uD56D\uBAA9\uC744 \uD55C \uBC88\uC5D0 \uBAA8\uC544 \uB118\uAE30\uC138\uC694(\uD56D\uBAA9\uB9C8\uB2E4 \uBC18\uBCF5 \uD638\uCD9C\uD558\uC9C0 \uB9C8\uC138\uC694). \uAC19\uC740 \uD56D\uBAA9\uC744 \uB2E4\uC2DC \uB123\uC5B4\uB3C4 \uC911\uBCF5 \uC5C6\uC774 \uAC31\uC2E0\uB429\uB2C8\uB2E4(\uACBD\uB825 achievements\uB294 \uAC00\uB2A5\uD558\uBA74 \uC815\uB7C9 \uC9C0\uD45C \uD3EC\uD568). \uC774 \uD56D\uBAA9\uB4E4\uC740 \uC628\uBCF4\uB529 \uC644\uC131\uB3C4(has_experience/has_skills)\uC5D0 \uBC18\uC601\uB429\uB2C8\uB2E4.
|
|
45842
45965
|
- \uD655\uC2E4\uD558\uC9C0 \uC54A\uC740 \uC815\uBCF4\uB294 \uC9C0\uC5B4\uB0B4\uC9C0 \uB9D0\uACE0 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uD655\uC778\uD558\uC138\uC694.
|
|
45843
45966
|
|
|
45844
45967
|
# 3\uB2E8\uACC4 \u2014 \uC774\uB825\uC11C \uB4F1\uB85D
|
|
@@ -45956,7 +46079,7 @@ var WORKFLOWS = [
|
|
|
45956
46079
|
'\uD30C\uC77C \uC785\uB825 \uBD84\uAE30: \u2460 \uC0AC\uC6A9\uC790\uAC00 "\uD30C\uC77C\uC774 \uC788\uB2E4"\uACE0 \uD558\uBA74 `open_inbox`\uB85C \uC778\uC785 \uD3F4\uB354\uB97C \uC5F4\uC5B4 \uD30C\uC77C\uC744 \uB123\uAC8C \uD558\uACE0, "\uB2E4 \uB123\uC5C8\uB2E4"\uACE0 \uD558\uBA74 `read_inbox`\uB85C \uBCF8\uBB38\uC744 \uC77D\uB294\uB2E4(pdf\xB7\uC774\uBBF8\uC9C0\uB294 read_inbox\uAC00 \uACBD\uB85C\uB9CC \uC8FC\uBBC0\uB85C \uD074\uB77C\uC774\uC5B8\uD2B8\uC758 \uD30C\uC77C \uC77D\uAE30\uB85C \uC9C1\uC811 \uC77D\uB294\uB2E4). \u2461 \uC0AC\uC6A9\uC790\uAC00 \uD30C\uC77C \uACBD\uB85C\uB97C \uC9C1\uC811 \uC54C\uB824\uC8FC\uBA74 `read_document`\uB85C \uC77D\uB294\uB2E4. \u2462 "\uC5C6\uB2E4/\uC9C1\uC811 \uC785\uB825/\uD14D\uC2A4\uD2B8\uB85C \uBD99\uC5EC\uB123\uAE30"\uBA74 \uD3F4\uB354\uB97C \uC5F4\uC9C0 \uB9D0\uACE0 \uBC1B\uC740 \uD14D\uC2A4\uD2B8\uB97C \uADF8\uB300\uB85C \uC4F4\uB2E4. (\uBB3B\uC9C0\uB3C4 \uC54A\uACE0 open_inbox\uB85C \uD3F4\uB354\uB97C \uC790\uB3D9\uC73C\uB85C \uC5F4\uC9C0 \uC54A\uB294\uB2E4.)',
|
|
45957
46080
|
"\uC218\uC9D1\uD55C \uC815\uBCF4\uB97C \uAD6C\uC870\uD654\uD574 `save_profile`\uB85C \uC800\uC7A5\uD55C\uB2E4(\uC774\uB984\xB7\uC5F0\uB77D\uCC98\xB7headline\xB7summary\xB7desired_roles\xB7desired_conditions, \uADF8\uB9AC\uACE0 \uAE00\uC4F0\uAE30 \uC120\uD638\uC778 preferred_tone\uACFC emphasis_points \uD3EC\uD568). \uD30C\uC77C \uCD9C\uCC98\uB294 source=upload, \uC9C1\uC811 \uC785\uB825\uC740 manual.",
|
|
45958
46081
|
"\uC5C5\uB85C\uB4DC\uD55C \uC774\uB825\uC11C/\uACBD\uB825\uAE30\uC220\uC11C/\uD3EC\uD2B8\uD3F4\uB9AC\uC624 \uBCF8\uBB38\uC744 `add_resume`\uB85C \uC800\uC7A5\uD55C\uB2E4(kind\xB7title \uC9C0\uC815, \uB300\uD45C \uBB38\uC11C\uB294 is_primary=true).",
|
|
45959
|
-
"\uACBD\uB825\xB7\uD504\uB85C\uC81D\uD2B8\xB7\uC2A4\uD0AC\uC774 \uD30C\uC545\uB418\uBA74 `add_experience`\xB7`add_project`\xB7`add_skill`\uB85C \
|
|
46082
|
+
"\uACBD\uB825\xB7\uD504\uB85C\uC81D\uD2B8\xB7\uC2A4\uD0AC\uC774 \uD30C\uC545\uB418\uBA74 `add_experience`\xB7`add_project`\xB7`add_skill`\uB85C \uC800\uC7A5\uD55C\uB2E4 \u2014 \uAC01 \uB3C4\uAD6C\uB294 \uBC30\uC5F4\uC744 \uBC1B\uC73C\uBBC0\uB85C \uD30C\uC545\uD55C \uD56D\uBAA9\uC744 \uD55C \uBC88\uC5D0 \uBAA8\uC544 \uB118\uAE34\uB2E4(\uD56D\uBAA9\uB9C8\uB2E4 \uBC18\uBCF5 \uD638\uCD9C\uD558\uC9C0 \uC54A\uB294\uB2E4). \uAC19\uC740 \uD56D\uBAA9\uC744 \uB2E4\uC2DC \uB123\uC5B4\uB3C4 \uC911\uBCF5 \uC5C6\uC774 \uAC31\uC2E0\uB41C\uB2E4(\uC644\uC131\uB3C4\uC758 has_experience/has_skills\uC5D0 \uBC18\uC601).",
|
|
45960
46083
|
"\uAE30\uC874 \uC790\uAE30\uC18C\uAC1C\uC11C\uAC00 \uC788\uC73C\uBA74 `save_cover_letter_version`\uC73C\uB85C \uC800\uC7A5\uD55C\uB2E4(title+content\uBA74 v1\uB85C \uC800\uC7A5, \uC5C6\uC73C\uBA74 \uB098\uC911\uC5D0 \uC791\uC131 \uAC00\uB2A5\uD568\uC744 \uC548\uB0B4).",
|
|
45961
46084
|
"`open_dashboard`\uB97C \uD638\uCD9C\uD574 \uC0AC\uC6A9\uC790\uAC00 \uC790\uAE30 \uB370\uC774\uD130\uB97C \uD655\uC778\uD558\uAC8C \uD55C\uB2E4.",
|
|
45962
46085
|
"\uB2E4\uC2DC `get_onboarding_status`\uB85C \uC644\uC131\uB3C4\uB97C \uD655\uC778\uD558\uACE0, \uB2E4\uC74C \uB2E8\uACC4(\uACF5\uACE0 \uBD84\uC11D \uB4F1)\uB97C \uC81C\uC548\uD55C\uB2E4."
|
|
@@ -47118,6 +47241,8 @@ function createServer() {
|
|
|
47118
47241
|
} catch (err) {
|
|
47119
47242
|
if (err instanceof HttpError) {
|
|
47120
47243
|
sendJson(res, err.status, { error: err.message, code: err.code });
|
|
47244
|
+
} else if (err instanceof CareerMateError) {
|
|
47245
|
+
sendJson(res, err.httpStatus, { error: err.message, code: err.code });
|
|
47121
47246
|
} else {
|
|
47122
47247
|
const message = err instanceof Error ? err.message : "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958";
|
|
47123
47248
|
console.error("[careermate] \uC694\uCCAD \uCC98\uB9AC \uC911 \uC624\uB958:", message);
|
package/docs/MCP_TOOLS.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
CareerMate는 **MCP-우선** 로컬 커리어 관리 도구입니다. 사용자는 자신의 ChatGPT/Claude/Gemini와 대화하고, 그 AI가 여기 정의된 MCP 도구를 호출해 로컬 커리어 DB(`~/.careermate/careermate.sqlite`)를 읽고 씁니다. **CareerMate 내부에는 LLM이 없습니다.** 적합도 분석·자기소개서 작성 같은 추론은 모두 사용자의 AI가 수행하고, CareerMate는 데이터를 저장하고 다시 돌려주는 역할만 합니다.
|
|
4
4
|
|
|
5
|
-
이 문서는 `packages/mcp-tools/src/tools.ts`에 정의된 커리어 워크플로 **
|
|
5
|
+
이 문서는 `packages/mcp-tools/src/tools.ts`에 정의된 커리어 워크플로 **35개 도구**의 레퍼런스입니다. 도구의 입력 파라미터는 `packages/shared/src/schemas.ts`의 zod 스키마와 일치합니다.
|
|
6
6
|
|
|
7
7
|
> 설계 원칙: 이 도구들은 단순 CRUD 래퍼가 아닙니다. 각 도구는 AI가 사용자를 대신해 수행하는 실제 작업에 대응하며, 설명에 *언제* 그리고 *어떻게* 호출해야 하는지가 담겨 있습니다. 가장 중요한 도구는 [`get_application_context`](#get_application_context-) 입니다.
|
|
8
8
|
|
|
@@ -16,7 +16,7 @@ CareerMate는 **MCP-우선** 로컬 커리어 관리 도구입니다. 사용자
|
|
|
16
16
|
|
|
17
17
|
| 읽기전용(✅) 도구 | 쓰기/부수효과(✏️) 도구 |
|
|
18
18
|
| --- | --- |
|
|
19
|
-
| `read_document`, `read_inbox`, `get_onboarding_status`, `get_profile`, `get_resumes`, `get_experiences`, `get_projects`, `get_skills`, `get_cover_letters`, `get_job_posting`, `list_jobs`, `get_application_context`, `list_recent_activity`, `get_workflow_guide`, `get_playbook`, `get_verifier`, `get_writing_style_guide`, `check_for_update` | `open_inbox`, `save_profile`, `add_resume`, `add_experience`, `add_project`, `add_skill`, `save_cover_letter_version`, `save_job_posting`, `save_fit_analysis`, `update_application_status`, `save_interview_prep`, `export_cover_letter`, `open_dashboard`, `open_application`, `update_careermate` |
|
|
19
|
+
| `read_document`, `read_inbox`, `get_onboarding_status`, `get_profile`, `get_resumes`, `get_experiences`, `get_projects`, `get_skills`, `get_cover_letters`, `get_job_posting`, `list_jobs`, `get_application_context`, `list_recent_activity`, `get_workflow_guide`, `get_playbook`, `get_verifier`, `validate_cover_letter`, `get_writing_style_guide`, `check_for_update` | `open_inbox`, `save_profile`, `add_resume`, `add_experience`, `add_project`, `add_skill`, `save_cover_letter_version`, `set_verify_mode`, `save_job_posting`, `save_fit_analysis`, `update_application_status`, `save_interview_prep`, `export_cover_letter`, `open_dashboard`, `open_application`, `update_careermate` |
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
@@ -97,16 +97,17 @@ AI 클라이언트가 연결되면 이 도구를 가장 먼저 호출해 상태
|
|
|
97
97
|
|
|
98
98
|
| 도구 | 목적 | 주요 입력 | 반환 | 언제 호출 | 읽기전용 |
|
|
99
99
|
| --- | --- | --- | --- | --- | --- |
|
|
100
|
-
| `add_experience` | 경력(직장 단위) 항목 저장 | `company`(필수), `role?`, `employment_type?`, `start_date?`, `end_date?`, `is_current?`, `description?`, `achievements?`, `tech?`, `order_index?` |
|
|
100
|
+
| `add_experience` | 경력(직장 단위) 항목 저장 | `experiences[]` — 각 항목: `company`(필수), `role?`, `employment_type?`, `start_date?`, `end_date?`, `is_current?`, `description?`, `achievements?`, `tech?`, `order_index?` | `ExperienceRecord[]` + 신규/갱신 건수 | 이력서에서 경력을 구조화해 보관할 때(여러 건 한 번에) | ✏️ |
|
|
101
101
|
| `get_experiences` | 저장된 경력을 시간 역순으로 조회 | 없음 | `ExperienceRecord[]` | 경력 목록만 빠르게 확인할 때 | ✅ |
|
|
102
|
-
| `add_project` | 대표 프로젝트 항목 저장 | `name`(필수), `role?`, `description?`, `highlights?`, `tech?`, `url?`, `start_date?`, `end_date?`, `order_index?` |
|
|
102
|
+
| `add_project` | 대표 프로젝트 항목 저장 | `projects[]` — 각 항목: `name`(필수), `role?`, `description?`, `highlights?`, `tech?`, `url?`, `start_date?`, `end_date?`, `order_index?` | `ProjectRecord[]` + 신규/갱신 건수 | 프로젝트 이력을 구조화해 보관할 때(여러 건 한 번에) | ✏️ |
|
|
103
103
|
| `get_projects` | 저장된 프로젝트 조회 | 없음 | `ProjectRecord[]` | 프로젝트 목록만 확인할 때 | ✅ |
|
|
104
|
-
| `add_skill` | 기술스택 항목 저장
|
|
104
|
+
| `add_skill` | 기술스택 항목 저장 | `skills[]` — 각 항목: `name`(필수), `category?`, `level?`, `years?`, `order_index?` | `SkillRecord[]` + 신규/갱신 건수 | 보유 기술을 정리해 둘 때(여러 개 한 번에) | ✏️ |
|
|
105
105
|
| `get_skills` | 저장된 기술스택 조회 | 없음 | `SkillRecord[]` | 기술 목록만 확인할 때 | ✅ |
|
|
106
106
|
|
|
107
|
+
- 세 도구 모두 **배열을 받아 여러 건을 한 번에 저장**합니다(한 건이어도 배열로). 항목마다 도구를 반복 호출하지 마세요.
|
|
108
|
+
- **멱등 저장**: 같은 항목(스킬=`name`, 경력=`company`+`role`+`start_date`, 프로젝트=`name`, 대소문자 무시)을 다시 넣어도 중복 행을 만들지 않고 갱신합니다. 누락한 필드는 기존 값을 보존합니다. 한 번의 호출은 한 트랜잭션으로 원자 처리됩니다.
|
|
107
109
|
- `achievements`/`highlights`에는 정량 지표가 담긴 성과를, `tech`에는 사용 기술을 배열로 넣으면 적합도 분석·자소서·면접 준비에서 근거로 활용됩니다.
|
|
108
|
-
-
|
|
109
|
-
- 입력 스키마: `ExperienceInputSchema` / `ProjectInputSchema` / `SkillInputSchema`(`packages/shared/src/schemas.ts`).
|
|
110
|
+
- 입력 스키마: `ExperienceBatchInputSchema` / `ProjectBatchInputSchema` / `SkillBatchInputSchema`(각각 `experiences`/`projects`/`skills` 배열, 요소는 `ExperienceInputSchema` / `ProjectInputSchema` / `SkillInputSchema`; `packages/shared/src/schemas.ts`).
|
|
110
111
|
|
|
111
112
|
---
|
|
112
113
|
|
|
@@ -248,6 +249,20 @@ AI 클라이언트가 연결되면 이 도구를 가장 먼저 호출해 상태
|
|
|
248
249
|
|
|
249
250
|
---
|
|
250
251
|
|
|
252
|
+
## 12-2. 자동 점검 / 점검 모드 (Deterministic Lint)
|
|
253
|
+
|
|
254
|
+
자기소개서를 저장하기 직전, CareerMate가 **LLM 없이 결정론으로** 셀 수 있는 항목만 점검합니다(정량 수치의 근거 유무, 공고 수치 차용, 번역투·클리셰 같은 문체 신호). 사실성·동문서답 같은 의미 판단은 연결된 AI가 직접 합니다.
|
|
255
|
+
|
|
256
|
+
| 도구 | 목적 | 주요 입력 | 반환 | 언제 호출 | 읽기전용 |
|
|
257
|
+
| --- | --- | --- | --- | --- | --- |
|
|
258
|
+
| `validate_cover_letter` | 저장하지 않고 미리 점검만 — 저장 시 차단될 항목·문체 신호를 보여줌 | `text`(필수), `job_id?`, `strict?` | 점검 리포트(`blocking[]`·신호·수치 출처 분석) | `save_cover_letter_version`으로 저장하기 직전 미리 확인할 때 | ✅ |
|
|
259
|
+
| `set_verify_mode` | 저장 전 자동 점검의 엄격도 변경(기본/엄격). 엄격 모드는 이력서 본문에 근거 없는 수치까지 차단 | `strict`(true=엄격, false=끄기) | `{ strict }` | 사용자가 "엄격하게 봐줘"에 동의해 모드를 바꿀 때(이번 한 건만이면 모드 대신 `strict:true`를 인자로) | ✏️ |
|
|
260
|
+
|
|
261
|
+
- `validate_cover_letter`의 `blocking`이 비어 있지 않으면, 같은 본문을 `save_cover_letter_version`으로 저장할 때도 막힙니다(의도한 값이면 `force:true`로 저장).
|
|
262
|
+
- `set_verify_mode`로 끄기(`strict:false`)는 안전장치를 약화하므로 사용자가 명확히 끄겠다고 할 때만 호출하세요.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
251
266
|
## 13. 글쓰기 스타일 가이드 (Writing Style Guide)
|
|
252
267
|
|
|
253
268
|
| 도구 | 목적 | 주요 입력 | 반환 | 언제 호출 | 읽기전용 |
|
package/docs/START_WORKFLOW.md
CHANGED
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
### 3-1. 경력·프로젝트·스킬 등록 (있는 경우) — `add_experience` · `add_project` · `add_skill`
|
|
39
39
|
|
|
40
40
|
- 이력서에서 파악되는 경력·프로젝트·보유 기술은 본문 저장과 별개로 `add_experience` / `add_project` / `add_skill`로 항목 단위 저장하면 적합도 분석·자소서·면접 준비에서 정밀하게 활용됩니다.
|
|
41
|
-
-
|
|
41
|
+
- 세 도구는 모두 배열을 받습니다 — 파악한 경력/프로젝트/기술을 **한 번에 모아** 넘기세요(항목마다 반복 호출하지 않습니다). 같은 항목을 다시 넣어도 중복 없이 갱신됩니다.
|
|
42
|
+
- 경력 `achievements`에는 가능하면 정량 지표를 넣으세요.
|
|
42
43
|
- 이 항목들은 `get_onboarding_status`의 `has_experience` / `has_skills`와 프로필 완성도(0~100)에 반영됩니다.
|
|
43
44
|
|
|
44
45
|
### 4. 자기소개서 등록 (있는 경우) — `save_cover_letter_version`
|
package/docs/WORKFLOWS.md
CHANGED
|
@@ -41,7 +41,7 @@ CareerMate는 내부에 LLM을 두지 않는다. 분석과 작성은 사용자
|
|
|
41
41
|
2. 비어 있는 항목을 사용자에게 쉬운 한국어로 안내하고, 이력서/자기소개서 파일 업로드 또는 정보 입력을 요청한다.
|
|
42
42
|
3. 수집한 정보를 구조화해 `save_profile`로 저장한다(이름·연락처·`headline`·`summary`·`desired_roles`·`desired_conditions`, 그리고 글쓰기 선호인 `preferred_tone`과 `emphasis_points` 포함). 파일 출처는 `source=upload`, 직접 입력은 `manual`.
|
|
43
43
|
4. 업로드한 이력서/경력기술서/포트폴리오 본문을 `add_resume`로 저장한다(`kind`·`title` 지정, 대표 문서는 `is_primary=true`).
|
|
44
|
-
5. 경력·프로젝트·스킬이 파악되면 `add_experience`·`add_project`·`add_skill`로
|
|
44
|
+
5. 경력·프로젝트·스킬이 파악되면 `add_experience`·`add_project`·`add_skill`로 저장한다 — 각 도구는 배열을 받으므로 파악한 항목을 한 번에 모아 넘긴다(항목마다 반복 호출하지 않는다). 같은 항목을 다시 넣어도 중복 없이 갱신된다(완성도의 `has_experience`/`has_skills`에 반영).
|
|
45
45
|
6. 기존 자기소개서가 있으면 `save_cover_letter_version`으로 저장한다(`title`+`content`면 v1로 저장, 없으면 나중에 작성 가능함을 안내).
|
|
46
46
|
7. `open_dashboard`를 호출해 사용자가 자기 데이터를 확인하게 한다.
|
|
47
47
|
8. 다시 `get_onboarding_status`로 완성도를 확인하고, 다음 단계(공고 분석 등)를 제안한다.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "careermate",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CareerMate — an MCP-first, local-first career workspace. Connect it to a local AI agent (Claude Desktop, Claude Code, Codex); CareerMate stores your career data locally on your machine and exposes it over MCP.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"shots:seed": "node --no-warnings --experimental-sqlite --import tsx scripts/shots-seed.ts",
|
|
67
67
|
"shots": "node --no-warnings --experimental-sqlite --import tsx scripts/shots.ts",
|
|
68
68
|
"doctor": "node --no-warnings --experimental-sqlite --import tsx scripts/doctor.ts",
|
|
69
|
-
"test": "node --no-warnings scripts/run.mjs scripts/test.ts \"TEST_VERDICT PASS\"",
|
|
69
|
+
"test": "node --no-warnings scripts/run.mjs scripts/test-init.ts \"INIT_TEST_VERDICT PASS\" && node --no-warnings scripts/run.mjs scripts/test.ts \"TEST_VERDICT PASS\"",
|
|
70
70
|
"test:ui": "node --no-warnings scripts/run.mjs scripts/ui-smoke.ts \"UI_VERDICT PASS\"",
|
|
71
71
|
"typecheck": "tsc --noEmit"
|
|
72
72
|
},
|
|
@@ -28,6 +28,9 @@ import {
|
|
|
28
28
|
type SkillRecord,
|
|
29
29
|
APPLICATION_STATUS_LABELS,
|
|
30
30
|
INTERVIEW_UNLOCK_STATUSES,
|
|
31
|
+
CareerMateError,
|
|
32
|
+
notFound,
|
|
33
|
+
conflict,
|
|
31
34
|
} from '@careermate/shared';
|
|
32
35
|
import {
|
|
33
36
|
jobRepo,
|
|
@@ -126,6 +129,65 @@ export function addSkill(input: SkillInput): SkillRecord {
|
|
|
126
129
|
return skill;
|
|
127
130
|
}
|
|
128
131
|
|
|
132
|
+
/* ---------- Batch variants: add many in one call (idempotent upsert) ---------- */
|
|
133
|
+
// The MCP tools (add_experience/add_project/add_skill) accept an array so the AI
|
|
134
|
+
// can save everything extracted from a résumé in a single call instead of looping
|
|
135
|
+
// once per item. The repos dedupe by natural key (company+role+start / name) and
|
|
136
|
+
// merge non-destructively, so re-extracting the same data doesn't create dupes.
|
|
137
|
+
// One activity entry summarizes the whole batch (names only — never body text).
|
|
138
|
+
|
|
139
|
+
/** Truncated, name-only summary suffix for batch activity logs (privacy-safe). */
|
|
140
|
+
function batchNames(names: string[]): string {
|
|
141
|
+
const shown = names.filter(Boolean).slice(0, 8).join(', ');
|
|
142
|
+
if (!shown) return '';
|
|
143
|
+
return `: ${shown}${names.length > 8 ? ' 외' : ''}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function addExperiences(inputs: ExperienceInput[]): {
|
|
147
|
+
records: ExperienceRecord[];
|
|
148
|
+
created: number;
|
|
149
|
+
updated: number;
|
|
150
|
+
} {
|
|
151
|
+
const res = experienceRepo.addMany(inputs);
|
|
152
|
+
activityRepo.log(
|
|
153
|
+
'profile_updated',
|
|
154
|
+
`경력 ${res.records.length}건을 저장했습니다(신규 ${res.created} · 갱신 ${res.updated})${batchNames(res.records.map((e) => e.company))}.`,
|
|
155
|
+
'experience',
|
|
156
|
+
res.records[0]?.id,
|
|
157
|
+
);
|
|
158
|
+
return res;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function addProjects(inputs: ProjectInput[]): {
|
|
162
|
+
records: ProjectRecord[];
|
|
163
|
+
created: number;
|
|
164
|
+
updated: number;
|
|
165
|
+
} {
|
|
166
|
+
const res = projectRepo.addMany(inputs);
|
|
167
|
+
activityRepo.log(
|
|
168
|
+
'profile_updated',
|
|
169
|
+
`프로젝트 ${res.records.length}건을 저장했습니다(신규 ${res.created} · 갱신 ${res.updated})${batchNames(res.records.map((p) => p.name))}.`,
|
|
170
|
+
'project',
|
|
171
|
+
res.records[0]?.id,
|
|
172
|
+
);
|
|
173
|
+
return res;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function addSkills(inputs: SkillInput[]): {
|
|
177
|
+
records: SkillRecord[];
|
|
178
|
+
created: number;
|
|
179
|
+
updated: number;
|
|
180
|
+
} {
|
|
181
|
+
const res = skillRepo.addMany(inputs);
|
|
182
|
+
activityRepo.log(
|
|
183
|
+
'profile_updated',
|
|
184
|
+
`기술 ${res.records.length}개를 저장했습니다(신규 ${res.created} · 갱신 ${res.updated})${batchNames(res.records.map((s) => s.name))}.`,
|
|
185
|
+
'skill',
|
|
186
|
+
res.records[0]?.id,
|
|
187
|
+
);
|
|
188
|
+
return res;
|
|
189
|
+
}
|
|
190
|
+
|
|
129
191
|
/* -------------------------------------------------------------------- Jobs */
|
|
130
192
|
|
|
131
193
|
/** Save (or update) a posting and make sure an application row tracks it. */
|
|
@@ -140,7 +202,7 @@ export function saveJobPosting(input: JobInput, id?: string): { job: JobRecord;
|
|
|
140
202
|
|
|
141
203
|
export function saveFitAnalysis(input: FitAnalysisInput): FitAnalysisRecord {
|
|
142
204
|
const job = jobRepo.get(input.job_id);
|
|
143
|
-
if (!job) throw
|
|
205
|
+
if (!job) throw notFound(`공고를 찾을 수 없습니다: ${input.job_id}. 먼저 save_job_posting으로 공고를 저장하세요.`);
|
|
144
206
|
applicationRepo.ensure(job.id, 'draft');
|
|
145
207
|
const fit = fitRepo.save(input);
|
|
146
208
|
const scoreText = fit.score != null ? ` (적합도 ${fit.score}점)` : '';
|
|
@@ -165,7 +227,7 @@ export function saveCoverLetterVersion(input: CoverLetterVersionInput & { force?
|
|
|
165
227
|
const tail = strictHit
|
|
166
228
|
? `엄격 모드가 켜져 있어, 올린 이력서 본문에 없는 수치는 막힙니다. 이력서를 올리거나 엄격 모드를 끄거나, 의도한 값이면 force: true로 저장하세요.`
|
|
167
229
|
: `이 수치는 저장된 경력·이력서·프로젝트에 없습니다. 원본 데이터의 실제 수치로 고치거나, 의도한 값이 맞다면 먼저 경력/프로젝트에 그 수치를 추가한 뒤 다시 저장하세요(그대로 저장하려면 force: true).`;
|
|
168
|
-
throw new
|
|
230
|
+
throw new CareerMateError(`저장 전 자동 점검에서 막혔습니다 — ${detail}. ${tail}`, 400, 'save_gate');
|
|
169
231
|
}
|
|
170
232
|
const { coverLetter, version } = coverLetterRepo.addVersion({
|
|
171
233
|
cover_letter_id: input.cover_letter_id,
|
|
@@ -207,7 +269,7 @@ export function updateApplicationStatus(
|
|
|
207
269
|
note?: string,
|
|
208
270
|
): StatusChangeResult {
|
|
209
271
|
const job = jobRepo.get(jobId);
|
|
210
|
-
if (!job) throw
|
|
272
|
+
if (!job) throw notFound(`공고를 찾을 수 없습니다: ${jobId}`);
|
|
211
273
|
const application = applicationRepo.setStatus(jobId, status, note);
|
|
212
274
|
activityRepo.log(
|
|
213
275
|
'application_status_changed',
|
|
@@ -230,13 +292,13 @@ export function updateApplicationStatus(
|
|
|
230
292
|
|
|
231
293
|
export function saveInterviewPrep(input: InterviewPrepInput): InterviewPrepRecord {
|
|
232
294
|
const job = jobRepo.get(input.job_id);
|
|
233
|
-
if (!job) throw
|
|
295
|
+
if (!job) throw notFound(`공고를 찾을 수 없습니다: ${input.job_id}`);
|
|
234
296
|
// Spec gate: interview prep is only allowed once the application has reached
|
|
235
297
|
// 서류 합격(document_passed) or beyond. Enforce it server-side, not just in prompts.
|
|
236
298
|
const app = applicationRepo.getByJob(job.id);
|
|
237
299
|
if (!app || !INTERVIEW_UNLOCK_STATUSES.includes(app.status)) {
|
|
238
300
|
const current = app ? APPLICATION_STATUS_LABELS[app.status] : '미지원';
|
|
239
|
-
throw
|
|
301
|
+
throw conflict(
|
|
240
302
|
`면접 준비는 '서류 합격' 이상 상태에서만 저장할 수 있습니다. 현재 상태: '${current}'. 먼저 update_application_status로 상태를 올린 뒤 다시 시도하세요.`,
|
|
241
303
|
);
|
|
242
304
|
}
|
|
Binary file
|
|
@@ -15,9 +15,9 @@ import fs from 'node:fs';
|
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
import {
|
|
17
17
|
ProfileInputSchema,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
ExperienceBatchInputSchema,
|
|
19
|
+
ProjectBatchInputSchema,
|
|
20
|
+
SkillBatchInputSchema,
|
|
21
21
|
JobInputSchema,
|
|
22
22
|
FitAnalysisInputSchema,
|
|
23
23
|
CoverLetterVersionInputSchema,
|
|
@@ -50,9 +50,9 @@ import {
|
|
|
50
50
|
getApplicationContext,
|
|
51
51
|
saveProfile,
|
|
52
52
|
addResume,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
addExperiences,
|
|
54
|
+
addProjects,
|
|
55
|
+
addSkills,
|
|
56
56
|
saveJobPosting,
|
|
57
57
|
saveFitAnalysis,
|
|
58
58
|
saveCoverLetterVersion,
|
|
@@ -343,11 +343,12 @@ export const TOOLS: ToolDef[] = [
|
|
|
343
343
|
name: 'add_experience',
|
|
344
344
|
title: '경력 추가',
|
|
345
345
|
description:
|
|
346
|
-
'구조화된 경력(직장 단위)을
|
|
347
|
-
inputSchema:
|
|
346
|
+
'구조화된 경력(직장 단위)을 저장합니다. 이력서에서 파악한 경력을 experiences 배열에 한 번에 담아 호출하세요(한 건이어도 배열로 — 항목마다 도구를 반복 호출하지 마세요). 이력서 본문(add_resume)과 달리 회사·직무·기간·성과를 필드로 나눠 저장해 적합도 분석/자소서 작성 시 정밀하게 활용됩니다. 각 항목은 company가 필수이며, achievements에는 정량 지표가 담긴 성과를, tech에는 사용 기술을 배열로 넣으세요. 같은 회사·직무·입사일(start_date)이 일치하면 갱신되고 중복 생성되지 않으니 여러 번 저장해도 안전합니다. 온보딩 완성도(get_onboarding_status)의 has_experience를 채우려면 1건 이상 추가하세요.',
|
|
347
|
+
inputSchema: ExperienceBatchInputSchema.shape,
|
|
348
348
|
handler: (args) => {
|
|
349
|
-
const
|
|
350
|
-
|
|
349
|
+
const { records, created, updated } = addExperiences(args.experiences);
|
|
350
|
+
const names = records.map((e) => `${e.company}${e.role ? ` · ${e.role}` : ''}`).join(', ');
|
|
351
|
+
return ok(`경력 ${records.length}건을 저장했습니다 (신규 ${created} · 갱신 ${updated}): ${names}`, records);
|
|
351
352
|
},
|
|
352
353
|
},
|
|
353
354
|
{
|
|
@@ -367,11 +368,12 @@ export const TOOLS: ToolDef[] = [
|
|
|
367
368
|
name: 'add_project',
|
|
368
369
|
title: '프로젝트 추가',
|
|
369
370
|
description:
|
|
370
|
-
'대표 프로젝트를
|
|
371
|
-
inputSchema:
|
|
371
|
+
'대표 프로젝트를 저장합니다. projects 배열에 여러 개를 한 번에 담아 호출하세요(한 개여도 배열로 — 항목마다 반복 호출하지 마세요). 각 항목은 name이 필수이며, highlights(핵심 성과)와 tech(사용 기술)를 배열로 넣으면 자소서·면접 준비에서 근거로 활용됩니다. url에 결과물 링크를 넣을 수 있습니다. 같은 이름의 프로젝트는 갱신되고 중복 생성되지 않으니 여러 번 저장해도 안전합니다.',
|
|
372
|
+
inputSchema: ProjectBatchInputSchema.shape,
|
|
372
373
|
handler: (args) => {
|
|
373
|
-
const
|
|
374
|
-
|
|
374
|
+
const { records, created, updated } = addProjects(args.projects);
|
|
375
|
+
const names = records.map((p) => p.name).join(', ');
|
|
376
|
+
return ok(`프로젝트 ${records.length}건을 저장했습니다 (신규 ${created} · 갱신 ${updated}): ${names}`, records);
|
|
375
377
|
},
|
|
376
378
|
},
|
|
377
379
|
{
|
|
@@ -391,11 +393,12 @@ export const TOOLS: ToolDef[] = [
|
|
|
391
393
|
name: 'add_skill',
|
|
392
394
|
title: '기술스택 추가',
|
|
393
395
|
description:
|
|
394
|
-
'보유 기술스택을
|
|
395
|
-
inputSchema:
|
|
396
|
+
'보유 기술스택을 저장합니다. 여러 개를 skills 배열에 한 번에 담아 호출하세요(한 개여도 배열로 — 기술마다 도구를 반복 호출하지 마세요). 각 항목은 name이 필수이며, category(언어/프레임워크/툴/소프트스킬 등)·level·years로 숙련도를 함께 기록하면 공고 키워드 매칭이 정확해집니다. 같은 이름의 기술은 갱신되고 중복 생성되지 않으니 여러 번 저장해도 안전합니다. 온보딩 완성도의 has_skills를 채우려면 1개 이상 추가하세요.',
|
|
397
|
+
inputSchema: SkillBatchInputSchema.shape,
|
|
396
398
|
handler: (args) => {
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
+
const { records, created, updated } = addSkills(args.skills);
|
|
400
|
+
const names = records.map((s) => s.name).join(', ');
|
|
401
|
+
return ok(`기술 ${records.length}개를 저장했습니다 (신규 ${created} · 갱신 ${updated}): ${names}`, records);
|
|
399
402
|
},
|
|
400
403
|
},
|
|
401
404
|
{
|
|
@@ -17,7 +17,7 @@ export const ONBOARDING_PROMPT = `이제 CareerMate에 연결되었습니다.
|
|
|
17
17
|
- 이름, 연락처, 한 줄 소개(headline), 자기소개 요약(summary), 희망 직무(desired_roles), 희망 근무 조건(desired_conditions)을 정리합니다.
|
|
18
18
|
- 글쓰기 선호도 함께 물어보세요: 선호 문체(preferred_tone, 예: "담백하고 구체적")와 강조하고 싶은 핵심 포인트(emphasis_points). 이 정보는 이후 자기소개서 작성에 그대로 활용됩니다.
|
|
19
19
|
- 정리한 내용을 \`save_profile\`로 저장하세요. 파일에서 읽어온 정보는 source를 'upload', 사용자가 말로 알려준 정보는 'manual'로 표시하세요.
|
|
20
|
-
- 경력·프로젝트·스킬 정보가 있으면 \`add_experience\`·\`add_project\`·\`add_skill\`로
|
|
20
|
+
- 경력·프로젝트·스킬 정보가 있으면 \`add_experience\`·\`add_project\`·\`add_skill\`로 저장하세요. 각 도구는 배열을 받으니 파악한 항목을 한 번에 모아 넘기세요(항목마다 반복 호출하지 마세요). 같은 항목을 다시 넣어도 중복 없이 갱신됩니다(경력 achievements는 가능하면 정량 지표 포함). 이 항목들은 온보딩 완성도(has_experience/has_skills)에 반영됩니다.
|
|
21
21
|
- 확실하지 않은 정보는 지어내지 말고 사용자에게 확인하세요.
|
|
22
22
|
|
|
23
23
|
# 3단계 — 이력서 등록
|