careermate 0.1.0 → 0.1.1
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 +10 -7
- package/apps/mcp/src/index.ts +3 -1
- package/apps/web/DESIGN_GUIDE.md +2 -2
- package/apps/web/public/icon-180.png +0 -0
- package/apps/web/public/index.html +4 -1
- package/apps/web/public/lib.js +1 -1
- package/apps/web/public/pages/applications.js +17 -1
- package/apps/web/public/pages/settings.js +6 -4
- package/apps/web/public/styles.css +49 -36
- package/apps/web/src/info.ts +9 -22
- package/apps/web/src/routes.ts +17 -13
- package/apps/web/src/server.ts +33 -25
- package/apps/web/src/settings.ts +35 -17
- package/bin/careermate.mjs +8 -5
- package/dist/{mcpb-stage/dist/web.mjs → doctor.mjs} +1122 -1450
- package/dist/install-page/favicon.svg +1 -0
- package/dist/install-page/icon-180.png +0 -0
- package/dist/install-page/icon-512.png +0 -0
- package/dist/install-page/index.html +309 -167
- package/dist/install-page/llms-install.txt +202 -0
- package/dist/install-page/og.png +0 -0
- package/dist/install-page/shots/applications-dark.png +0 -0
- package/dist/install-page/shots/applications-light.png +0 -0
- package/dist/install-page/shots/documents-dark.png +0 -0
- package/dist/install-page/shots/documents-light.png +0 -0
- package/dist/install-page/shots/home-dark.png +0 -0
- package/dist/install-page/shots/home-light.png +0 -0
- package/dist/install-page/shots/interview-dark.png +0 -0
- package/dist/install-page/shots/interview-light.png +0 -0
- package/dist/install-page/shots/jobs-dark.png +0 -0
- package/dist/install-page/shots/jobs-light.png +0 -0
- package/dist/install-page/shots/settings-dark.png +0 -0
- package/dist/install-page/shots/settings-light.png +0 -0
- package/dist/install-page/start.html +346 -0
- package/dist/install-page/style.css +94 -3
- package/dist/mcp.mjs +326 -68
- package/dist/migrate.mjs +4715 -0
- package/dist/public/icon-180.png +0 -0
- package/dist/public/index.html +4 -1
- package/dist/public/lib.js +1 -1
- package/dist/public/pages/applications.js +17 -1
- package/dist/public/pages/settings.js +6 -4
- package/dist/public/styles.css +49 -36
- package/dist/web.mjs +323 -129
- package/docs/DEMO.md +3 -3
- package/docs/MCP_TOOLS.md +21 -2
- package/docs/ROADMAP.md +3 -3
- package/docs/RUNTIME_DISTRIBUTION_RESEARCH.md +135 -0
- package/docs/START_WORKFLOW.md +6 -0
- package/docs/SUPPORTED_AI_APPS.md +85 -33
- package/docs/WORKFLOWS.md +1 -1
- package/package.json +3 -2
- package/packages/core/src/services.ts +42 -1
- package/packages/db/src/connection.ts +30 -0
- package/packages/db/src/index.ts +1 -0
- package/packages/db/src/repositories.ts +109 -63
- package/packages/db/src/schema.ts +118 -0
- package/packages/exporters/src/html.ts +9 -12
- package/packages/mcp-tools/src/tools.ts +81 -0
- package/packages/shared/src/enums.ts +39 -0
- package/scripts/build-dist.mjs +27 -8
- package/scripts/build-mcpb.mjs +60 -4
- package/scripts/gen-og.mjs +86 -0
- package/scripts/init.ts +32 -3
- package/scripts/seed.ts +5 -5
- package/scripts/shots-seed.ts +192 -0
- package/scripts/shots.ts +89 -0
- package/tsconfig.json +3 -1
- package/dist/careermate.mcpb +0 -0
- package/dist/mcp-smoke.err +0 -3
- package/dist/mcpb-stage/README.md +0 -219
- package/dist/mcpb-stage/dist/install-page/index.html +0 -434
- package/dist/mcpb-stage/dist/install-page/style.css +0 -407
- package/dist/mcpb-stage/dist/install-page/vercel.json +0 -20
- package/dist/mcpb-stage/dist/mcp.mjs +0 -23704
- package/dist/mcpb-stage/dist/public/app.js +0 -118
- package/dist/mcpb-stage/dist/public/index.html +0 -41
- package/dist/mcpb-stage/dist/public/lib.js +0 -282
- package/dist/mcpb-stage/dist/public/pages/applications.js +0 -98
- package/dist/mcpb-stage/dist/public/pages/documents.js +0 -446
- package/dist/mcpb-stage/dist/public/pages/home.js +0 -263
- package/dist/mcpb-stage/dist/public/pages/interview.js +0 -230
- package/dist/mcpb-stage/dist/public/pages/jobs.js +0 -494
- package/dist/mcpb-stage/dist/public/pages/profile.js +0 -576
- package/dist/mcpb-stage/dist/public/pages/settings.js +0 -233
- package/dist/mcpb-stage/dist/public/styles.css +0 -420
- package/dist/mcpb-stage/manifest.json +0 -40
- package/docs/INSTALL.md +0 -148
- package/docs/INSTALL_AND_USAGE.md +0 -99
- package/install-page/index.html +0 -474
- package/install-page/style.css +0 -391
- package/install-page/vercel.json +0 -20
- /package/dist/{mcpb-stage/dist/public → install-page}/fonts/PretendardVariable.woff2 +0 -0
package/dist/web.mjs
CHANGED
|
@@ -216,6 +216,123 @@ var MIGRATIONS = [
|
|
|
216
216
|
CREATE INDEX IF NOT EXISTS idx_clv_letter ON cover_letter_versions(cover_letter_id);
|
|
217
217
|
CREATE INDEX IF NOT EXISTS idx_cl_job ON cover_letters(job_id);
|
|
218
218
|
CREATE INDEX IF NOT EXISTS idx_activities_created ON activities(created_at DESC);
|
|
219
|
+
`,
|
|
220
|
+
// v2 — add foreign keys so PRAGMA foreign_keys=ON is actually enforced.
|
|
221
|
+
//
|
|
222
|
+
// SQLite cannot ALTER a table to add constraints, so each table that needs an
|
|
223
|
+
// FK is recreated (create new → copy → drop old → rename). The copy also
|
|
224
|
+
// repairs pre-existing orphans: child rows whose parent is gone are dropped
|
|
225
|
+
// (CASCADE intent) and dangling optional refs are nulled (SET NULL intent), so
|
|
226
|
+
// a subsequent PRAGMA foreign_key_check stays clean. FKs are disabled for the
|
|
227
|
+
// rebuild and the whole thing runs in one transaction (auto-rolled-back if the
|
|
228
|
+
// process dies mid-migration, since the version bump happens only afterwards).
|
|
229
|
+
`
|
|
230
|
+
PRAGMA foreign_keys=OFF;
|
|
231
|
+
BEGIN;
|
|
232
|
+
|
|
233
|
+
-- cover_letters.job_id -> jobs(id) ON DELETE SET NULL
|
|
234
|
+
CREATE TABLE cover_letters_new (
|
|
235
|
+
id TEXT PRIMARY KEY,
|
|
236
|
+
title TEXT NOT NULL,
|
|
237
|
+
job_id TEXT REFERENCES jobs(id) ON DELETE SET NULL,
|
|
238
|
+
is_primary INTEGER NOT NULL DEFAULT 0,
|
|
239
|
+
current_version_id TEXT,
|
|
240
|
+
created_at TEXT NOT NULL,
|
|
241
|
+
updated_at TEXT NOT NULL
|
|
242
|
+
);
|
|
243
|
+
INSERT INTO cover_letters_new (id,title,job_id,is_primary,current_version_id,created_at,updated_at)
|
|
244
|
+
SELECT id,title,
|
|
245
|
+
CASE WHEN job_id IN (SELECT id FROM jobs) THEN job_id ELSE NULL END,
|
|
246
|
+
is_primary,current_version_id,created_at,updated_at
|
|
247
|
+
FROM cover_letters;
|
|
248
|
+
DROP TABLE cover_letters;
|
|
249
|
+
ALTER TABLE cover_letters_new RENAME TO cover_letters;
|
|
250
|
+
|
|
251
|
+
-- cover_letter_versions.cover_letter_id -> cover_letters(id) ON DELETE CASCADE
|
|
252
|
+
CREATE TABLE cover_letter_versions_new (
|
|
253
|
+
id TEXT PRIMARY KEY,
|
|
254
|
+
cover_letter_id TEXT NOT NULL REFERENCES cover_letters(id) ON DELETE CASCADE,
|
|
255
|
+
version_no INTEGER NOT NULL,
|
|
256
|
+
content TEXT NOT NULL,
|
|
257
|
+
note TEXT,
|
|
258
|
+
source TEXT NOT NULL DEFAULT 'ai',
|
|
259
|
+
created_at TEXT NOT NULL
|
|
260
|
+
);
|
|
261
|
+
INSERT INTO cover_letter_versions_new (id,cover_letter_id,version_no,content,note,source,created_at)
|
|
262
|
+
SELECT id,cover_letter_id,version_no,content,note,source,created_at
|
|
263
|
+
FROM cover_letter_versions
|
|
264
|
+
WHERE cover_letter_id IN (SELECT id FROM cover_letters);
|
|
265
|
+
DROP TABLE cover_letter_versions;
|
|
266
|
+
ALTER TABLE cover_letter_versions_new RENAME TO cover_letter_versions;
|
|
267
|
+
|
|
268
|
+
-- fit_analyses.job_id -> jobs(id) ON DELETE CASCADE
|
|
269
|
+
CREATE TABLE fit_analyses_new (
|
|
270
|
+
id TEXT PRIMARY KEY,
|
|
271
|
+
job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
|
|
272
|
+
score REAL,
|
|
273
|
+
summary TEXT,
|
|
274
|
+
strengths TEXT NOT NULL DEFAULT '[]',
|
|
275
|
+
gaps TEXT NOT NULL DEFAULT '[]',
|
|
276
|
+
matched_keywords TEXT NOT NULL DEFAULT '[]',
|
|
277
|
+
missing_keywords TEXT NOT NULL DEFAULT '[]',
|
|
278
|
+
recommendations TEXT NOT NULL DEFAULT '[]',
|
|
279
|
+
created_at TEXT NOT NULL,
|
|
280
|
+
updated_at TEXT NOT NULL
|
|
281
|
+
);
|
|
282
|
+
INSERT INTO fit_analyses_new (id,job_id,score,summary,strengths,gaps,matched_keywords,missing_keywords,recommendations,created_at,updated_at)
|
|
283
|
+
SELECT id,job_id,score,summary,strengths,gaps,matched_keywords,missing_keywords,recommendations,created_at,updated_at
|
|
284
|
+
FROM fit_analyses
|
|
285
|
+
WHERE job_id IN (SELECT id FROM jobs);
|
|
286
|
+
DROP TABLE fit_analyses;
|
|
287
|
+
ALTER TABLE fit_analyses_new RENAME TO fit_analyses;
|
|
288
|
+
|
|
289
|
+
-- applications: job_id CASCADE, resume_id/cover_letter_id SET NULL
|
|
290
|
+
CREATE TABLE applications_new (
|
|
291
|
+
id TEXT PRIMARY KEY,
|
|
292
|
+
job_id TEXT NOT NULL UNIQUE REFERENCES jobs(id) ON DELETE CASCADE,
|
|
293
|
+
status TEXT NOT NULL DEFAULT 'draft',
|
|
294
|
+
resume_id TEXT REFERENCES documents(id) ON DELETE SET NULL,
|
|
295
|
+
cover_letter_id TEXT REFERENCES cover_letters(id) ON DELETE SET NULL,
|
|
296
|
+
applied_at TEXT,
|
|
297
|
+
notes TEXT,
|
|
298
|
+
created_at TEXT NOT NULL,
|
|
299
|
+
updated_at TEXT NOT NULL
|
|
300
|
+
);
|
|
301
|
+
INSERT INTO applications_new (id,job_id,status,resume_id,cover_letter_id,applied_at,notes,created_at,updated_at)
|
|
302
|
+
SELECT id,job_id,status,
|
|
303
|
+
CASE WHEN resume_id IN (SELECT id FROM documents) THEN resume_id ELSE NULL END,
|
|
304
|
+
CASE WHEN cover_letter_id IN (SELECT id FROM cover_letters) THEN cover_letter_id ELSE NULL END,
|
|
305
|
+
applied_at,notes,created_at,updated_at
|
|
306
|
+
FROM applications
|
|
307
|
+
WHERE job_id IN (SELECT id FROM jobs);
|
|
308
|
+
DROP TABLE applications;
|
|
309
|
+
ALTER TABLE applications_new RENAME TO applications;
|
|
310
|
+
|
|
311
|
+
-- interview_preps.job_id -> jobs(id) ON DELETE CASCADE
|
|
312
|
+
CREATE TABLE interview_preps_new (
|
|
313
|
+
id TEXT PRIMARY KEY,
|
|
314
|
+
job_id TEXT NOT NULL UNIQUE REFERENCES jobs(id) ON DELETE CASCADE,
|
|
315
|
+
questions TEXT NOT NULL DEFAULT '[]',
|
|
316
|
+
star_guides TEXT NOT NULL DEFAULT '[]',
|
|
317
|
+
self_introduction TEXT,
|
|
318
|
+
notes TEXT,
|
|
319
|
+
created_at TEXT NOT NULL,
|
|
320
|
+
updated_at TEXT NOT NULL
|
|
321
|
+
);
|
|
322
|
+
INSERT INTO interview_preps_new (id,job_id,questions,star_guides,self_introduction,notes,created_at,updated_at)
|
|
323
|
+
SELECT id,job_id,questions,star_guides,self_introduction,notes,created_at,updated_at
|
|
324
|
+
FROM interview_preps
|
|
325
|
+
WHERE job_id IN (SELECT id FROM jobs);
|
|
326
|
+
DROP TABLE interview_preps;
|
|
327
|
+
ALTER TABLE interview_preps_new RENAME TO interview_preps;
|
|
328
|
+
|
|
329
|
+
-- Recreate indexes dropped along with their tables.
|
|
330
|
+
CREATE INDEX IF NOT EXISTS idx_fit_job ON fit_analyses(job_id);
|
|
331
|
+
CREATE INDEX IF NOT EXISTS idx_clv_letter ON cover_letter_versions(cover_letter_id);
|
|
332
|
+
CREATE INDEX IF NOT EXISTS idx_cl_job ON cover_letters(job_id);
|
|
333
|
+
|
|
334
|
+
COMMIT;
|
|
335
|
+
PRAGMA foreign_keys=ON;
|
|
219
336
|
`
|
|
220
337
|
];
|
|
221
338
|
function migrate(db) {
|
|
@@ -247,6 +364,26 @@ function getDb() {
|
|
|
247
364
|
_db = db;
|
|
248
365
|
return db;
|
|
249
366
|
}
|
|
367
|
+
var _inTx = false;
|
|
368
|
+
function tx(fn) {
|
|
369
|
+
if (_inTx) return fn();
|
|
370
|
+
const db = getDb();
|
|
371
|
+
db.exec("BEGIN");
|
|
372
|
+
_inTx = true;
|
|
373
|
+
try {
|
|
374
|
+
const result = fn();
|
|
375
|
+
db.exec("COMMIT");
|
|
376
|
+
return result;
|
|
377
|
+
} catch (err) {
|
|
378
|
+
try {
|
|
379
|
+
db.exec("ROLLBACK");
|
|
380
|
+
} catch {
|
|
381
|
+
}
|
|
382
|
+
throw err;
|
|
383
|
+
} finally {
|
|
384
|
+
_inTx = false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
250
387
|
function toJson(value) {
|
|
251
388
|
return JSON.stringify(value ?? null);
|
|
252
389
|
}
|
|
@@ -305,6 +442,27 @@ var INTERVIEW_UNLOCK_STATUSES = [
|
|
|
305
442
|
"interview",
|
|
306
443
|
"final_passed"
|
|
307
444
|
];
|
|
445
|
+
var ALLOWED_STATUS_TRANSITIONS = {
|
|
446
|
+
draft: ["planned", "applied", "document_passed", "interview", "final_passed", "rejected", "on_hold"],
|
|
447
|
+
planned: ["applied", "document_passed", "interview", "final_passed", "rejected", "on_hold"],
|
|
448
|
+
applied: ["planned", "document_passed", "interview", "final_passed", "rejected", "on_hold"],
|
|
449
|
+
document_passed: ["planned", "applied", "interview", "final_passed", "rejected", "on_hold"],
|
|
450
|
+
interview: ["planned", "applied", "document_passed", "final_passed", "rejected", "on_hold"],
|
|
451
|
+
final_passed: ["document_passed", "interview", "rejected", "on_hold"],
|
|
452
|
+
rejected: ["draft", "planned", "applied", "document_passed", "interview", "final_passed", "on_hold"],
|
|
453
|
+
on_hold: ["draft", "planned", "applied", "document_passed", "interview", "final_passed", "rejected"]
|
|
454
|
+
};
|
|
455
|
+
function canTransitionStatus(from, to) {
|
|
456
|
+
if (from === to) return true;
|
|
457
|
+
return ALLOWED_STATUS_TRANSITIONS[from]?.includes(to) ?? false;
|
|
458
|
+
}
|
|
459
|
+
function assertStatusTransition(from, to) {
|
|
460
|
+
if (!canTransitionStatus(from, to)) {
|
|
461
|
+
throw new Error(
|
|
462
|
+
`'${APPLICATION_STATUS_LABELS[from]}'\uC5D0\uC11C '${APPLICATION_STATUS_LABELS[to]}'(\uC73C)\uB85C\uB294 \uC0C1\uD0DC\uB97C \uBC14\uAFC0 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
308
466
|
var DOCUMENT_KINDS = ["resume", "career_description", "portfolio", "other"];
|
|
309
467
|
var DOCUMENT_KIND_LABELS = {
|
|
310
468
|
resume: "\uC774\uB825\uC11C",
|
|
@@ -4969,34 +5127,38 @@ var documentRepo = {
|
|
|
4969
5127
|
const ts = now();
|
|
4970
5128
|
const id3 = newId("doc_");
|
|
4971
5129
|
const db = getDb();
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
5130
|
+
return tx(() => {
|
|
5131
|
+
if (input.is_primary) {
|
|
5132
|
+
db.prepare(`UPDATE documents SET is_primary=0 WHERE kind=?`).run(input.kind);
|
|
5133
|
+
}
|
|
5134
|
+
db.prepare(
|
|
5135
|
+
`INSERT INTO documents (id,kind,title,content,source,is_primary,tags,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?)`
|
|
5136
|
+
).run(
|
|
5137
|
+
id3,
|
|
5138
|
+
input.kind,
|
|
5139
|
+
input.title,
|
|
5140
|
+
input.content ?? "",
|
|
5141
|
+
input.source ?? "manual",
|
|
5142
|
+
toBit(input.is_primary),
|
|
5143
|
+
toJson(input.tags ?? []),
|
|
5144
|
+
ts,
|
|
5145
|
+
ts
|
|
5146
|
+
);
|
|
5147
|
+
return this.get(id3);
|
|
5148
|
+
});
|
|
4989
5149
|
},
|
|
4990
5150
|
update(id3, input) {
|
|
4991
5151
|
const cur = this.get(id3);
|
|
4992
5152
|
if (!cur) return null;
|
|
4993
5153
|
const db = getDb();
|
|
4994
5154
|
const m = { ...cur, ...input };
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5155
|
+
return tx(() => {
|
|
5156
|
+
if (input.is_primary) db.prepare(`UPDATE documents SET is_primary=0 WHERE kind=?`).run(m.kind);
|
|
5157
|
+
db.prepare(
|
|
5158
|
+
`UPDATE documents SET kind=?,title=?,content=?,source=?,is_primary=?,tags=?,updated_at=? WHERE id=?`
|
|
5159
|
+
).run(m.kind, m.title, m.content, m.source, toBit(m.is_primary), toJson(m.tags), now(), id3);
|
|
5160
|
+
return this.get(id3);
|
|
5161
|
+
});
|
|
5000
5162
|
},
|
|
5001
5163
|
remove(id3) {
|
|
5002
5164
|
return getDb().prepare(`DELETE FROM documents WHERE id=?`).run(id3).changes > 0;
|
|
@@ -5045,37 +5207,41 @@ var coverLetterRepo = {
|
|
|
5045
5207
|
const ts = now();
|
|
5046
5208
|
const id3 = newId("cl_");
|
|
5047
5209
|
const db = getDb();
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5210
|
+
return tx(() => {
|
|
5211
|
+
if (opts.is_primary) db.prepare(`UPDATE cover_letters SET is_primary=0`).run();
|
|
5212
|
+
db.prepare(
|
|
5213
|
+
`INSERT INTO cover_letters (id,title,job_id,is_primary,current_version_id,created_at,updated_at) VALUES (?,?,?,?,?,?,?)`
|
|
5214
|
+
).run(id3, opts.title, opts.job_id ?? null, toBit(opts.is_primary), null, ts, ts);
|
|
5215
|
+
return this.get(id3);
|
|
5216
|
+
});
|
|
5053
5217
|
},
|
|
5054
5218
|
/** Append a new version. Creates the cover letter if cover_letter_id is omitted. */
|
|
5055
5219
|
addVersion(opts) {
|
|
5056
5220
|
const db = getDb();
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5221
|
+
return tx(() => {
|
|
5222
|
+
let clId = opts.cover_letter_id;
|
|
5223
|
+
if (!clId) {
|
|
5224
|
+
const cl = this.create({ title: opts.title ?? "\uC790\uAE30\uC18C\uAC1C\uC11C", job_id: opts.job_id ?? null });
|
|
5225
|
+
clId = cl.id;
|
|
5226
|
+
}
|
|
5227
|
+
const maxRow = db.prepare(`SELECT MAX(version_no) AS m FROM cover_letter_versions WHERE cover_letter_id=?`).get(clId);
|
|
5228
|
+
const versionNo = (maxRow.m ?? 0) + 1;
|
|
5229
|
+
const vid = newId("clv_");
|
|
5230
|
+
const ts = now();
|
|
5231
|
+
db.prepare(
|
|
5232
|
+
`INSERT INTO cover_letter_versions (id,cover_letter_id,version_no,content,note,source,created_at) VALUES (?,?,?,?,?,?,?)`
|
|
5233
|
+
).run(vid, clId, versionNo, opts.content, opts.note ?? null, opts.source ?? "ai", ts);
|
|
5234
|
+
const setCurrent = opts.set_current !== false;
|
|
5235
|
+
if (setCurrent) {
|
|
5236
|
+
db.prepare(`UPDATE cover_letters SET current_version_id=?, updated_at=? WHERE id=?`).run(vid, ts, clId);
|
|
5237
|
+
} else {
|
|
5238
|
+
db.prepare(`UPDATE cover_letters SET updated_at=? WHERE id=?`).run(ts, clId);
|
|
5239
|
+
}
|
|
5240
|
+
if (opts.title) db.prepare(`UPDATE cover_letters SET title=? WHERE id=?`).run(opts.title, clId);
|
|
5241
|
+
if (opts.job_id !== void 0)
|
|
5242
|
+
db.prepare(`UPDATE cover_letters SET job_id=? WHERE id=?`).run(opts.job_id, clId);
|
|
5243
|
+
return { coverLetter: this.get(clId), version: mapVersion(db.prepare(`SELECT * FROM cover_letter_versions WHERE id=?`).get(vid)) };
|
|
5244
|
+
});
|
|
5079
5245
|
},
|
|
5080
5246
|
setCurrentVersion(coverLetterId, versionId) {
|
|
5081
5247
|
getDb().prepare(`UPDATE cover_letters SET current_version_id=?, updated_at=? WHERE id=?`).run(versionId, now(), coverLetterId);
|
|
@@ -5083,14 +5249,18 @@ var coverLetterRepo = {
|
|
|
5083
5249
|
},
|
|
5084
5250
|
setPrimary(id3) {
|
|
5085
5251
|
const db = getDb();
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5252
|
+
return tx(() => {
|
|
5253
|
+
db.prepare(`UPDATE cover_letters SET is_primary=0`).run();
|
|
5254
|
+
db.prepare(`UPDATE cover_letters SET is_primary=1, updated_at=? WHERE id=?`).run(now(), id3);
|
|
5255
|
+
return this.get(id3);
|
|
5256
|
+
});
|
|
5089
5257
|
},
|
|
5090
5258
|
remove(id3) {
|
|
5091
5259
|
const db = getDb();
|
|
5092
|
-
|
|
5093
|
-
|
|
5260
|
+
return tx(() => {
|
|
5261
|
+
db.prepare(`DELETE FROM cover_letter_versions WHERE cover_letter_id=?`).run(id3);
|
|
5262
|
+
return db.prepare(`DELETE FROM cover_letters WHERE id=?`).run(id3).changes > 0;
|
|
5263
|
+
});
|
|
5094
5264
|
}
|
|
5095
5265
|
};
|
|
5096
5266
|
function mapJob(r) {
|
|
@@ -5175,10 +5345,12 @@ var jobRepo = {
|
|
|
5175
5345
|
},
|
|
5176
5346
|
remove(id3) {
|
|
5177
5347
|
const db = getDb();
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5348
|
+
return tx(() => {
|
|
5349
|
+
db.prepare(`DELETE FROM fit_analyses WHERE job_id=?`).run(id3);
|
|
5350
|
+
db.prepare(`DELETE FROM applications WHERE job_id=?`).run(id3);
|
|
5351
|
+
db.prepare(`DELETE FROM interview_preps WHERE job_id=?`).run(id3);
|
|
5352
|
+
return db.prepare(`DELETE FROM jobs WHERE id=?`).run(id3).changes > 0;
|
|
5353
|
+
});
|
|
5182
5354
|
}
|
|
5183
5355
|
};
|
|
5184
5356
|
function mapFit(r) {
|
|
@@ -5285,6 +5457,7 @@ var applicationRepo = {
|
|
|
5285
5457
|
},
|
|
5286
5458
|
upsert(input) {
|
|
5287
5459
|
const app = this.ensure(input.job_id, input.status ?? "draft");
|
|
5460
|
+
if (input.status && input.status !== app.status) assertStatusTransition(app.status, input.status);
|
|
5288
5461
|
const m = { ...app, ...input };
|
|
5289
5462
|
getDb().prepare(
|
|
5290
5463
|
`UPDATE applications SET status=?,resume_id=?,cover_letter_id=?,applied_at=?,notes=?,updated_at=? WHERE id=?`
|
|
@@ -5293,6 +5466,7 @@ var applicationRepo = {
|
|
|
5293
5466
|
},
|
|
5294
5467
|
setStatus(jobId, status, note) {
|
|
5295
5468
|
const app = this.ensure(jobId);
|
|
5469
|
+
assertStatusTransition(app.status, status);
|
|
5296
5470
|
const appliedAt = status === "applied" && !app.applied_at ? now() : app.applied_at;
|
|
5297
5471
|
const notes = note ? [app.notes, note].filter(Boolean).join("\n") : app.notes;
|
|
5298
5472
|
getDb().prepare(`UPDATE applications SET status=?, applied_at=?, notes=?, updated_at=? WHERE id=?`).run(status, appliedAt, notes, now(), app.id);
|
|
@@ -5368,6 +5542,21 @@ var activityRepo = {
|
|
|
5368
5542
|
return getDb().prepare(`SELECT * FROM activities ORDER BY created_at DESC LIMIT ?`).all(limit);
|
|
5369
5543
|
}
|
|
5370
5544
|
};
|
|
5545
|
+
function getEntityCounts() {
|
|
5546
|
+
const db = getDb();
|
|
5547
|
+
const c = (t) => db.prepare(`SELECT COUNT(*) AS n FROM ${t}`).get().n;
|
|
5548
|
+
return {
|
|
5549
|
+
profile: c("profile"),
|
|
5550
|
+
experiences: c("experiences"),
|
|
5551
|
+
projects: c("projects"),
|
|
5552
|
+
skills: c("skills"),
|
|
5553
|
+
documents: c("documents"),
|
|
5554
|
+
cover_letters: c("cover_letters"),
|
|
5555
|
+
jobs: c("jobs"),
|
|
5556
|
+
applications: c("applications"),
|
|
5557
|
+
interview_preps: c("interview_preps")
|
|
5558
|
+
};
|
|
5559
|
+
}
|
|
5371
5560
|
|
|
5372
5561
|
// apps/web/src/security.ts
|
|
5373
5562
|
import crypto from "node:crypto";
|
|
@@ -5677,6 +5866,21 @@ function addResume(input) {
|
|
|
5677
5866
|
activityRepo.log("resume_added", `${doc.title} \uBB38\uC11C\uB97C \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "document", doc.id);
|
|
5678
5867
|
return doc;
|
|
5679
5868
|
}
|
|
5869
|
+
function addExperience(input) {
|
|
5870
|
+
const exp = experienceRepo.add(input);
|
|
5871
|
+
activityRepo.log("profile_updated", `\uACBD\uB825(${exp.company})\uC744 \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "experience", exp.id);
|
|
5872
|
+
return exp;
|
|
5873
|
+
}
|
|
5874
|
+
function addProject(input) {
|
|
5875
|
+
const prj = projectRepo.add(input);
|
|
5876
|
+
activityRepo.log("profile_updated", `\uD504\uB85C\uC81D\uD2B8(${prj.name})\uB97C \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "project", prj.id);
|
|
5877
|
+
return prj;
|
|
5878
|
+
}
|
|
5879
|
+
function addSkill(input) {
|
|
5880
|
+
const skill = skillRepo.add(input);
|
|
5881
|
+
activityRepo.log("profile_updated", `\uAE30\uC220(${skill.name})\uC744 \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "skill", skill.id);
|
|
5882
|
+
return skill;
|
|
5883
|
+
}
|
|
5680
5884
|
function saveJobPosting(input, id3) {
|
|
5681
5885
|
const job = jobRepo.upsert(input, id3);
|
|
5682
5886
|
const application = applicationRepo.ensure(job.id, "draft");
|
|
@@ -5734,7 +5938,13 @@ function updateApplicationStatus(jobId, status, note) {
|
|
|
5734
5938
|
function saveInterviewPrep(input) {
|
|
5735
5939
|
const job = jobRepo.get(input.job_id);
|
|
5736
5940
|
if (!job) throw new Error(`\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.job_id}`);
|
|
5737
|
-
applicationRepo.
|
|
5941
|
+
const app = applicationRepo.getByJob(job.id);
|
|
5942
|
+
if (!app || !INTERVIEW_UNLOCK_STATUSES.includes(app.status)) {
|
|
5943
|
+
const current = app ? APPLICATION_STATUS_LABELS[app.status] : "\uBBF8\uC9C0\uC6D0";
|
|
5944
|
+
throw new Error(
|
|
5945
|
+
`\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.`
|
|
5946
|
+
);
|
|
5947
|
+
}
|
|
5738
5948
|
const prep = interviewRepo.save(input);
|
|
5739
5949
|
activityRepo.log("interview_prep_saved", `${job.company} \xB7 ${job.position} \uBA74\uC811 \uC900\uBE44 \uC790\uB8CC\uB97C \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4.`, "interview_prep", prep.id);
|
|
5740
5950
|
return prep;
|
|
@@ -6360,11 +6570,8 @@ function cleanJobPosting(raw) {
|
|
|
6360
6570
|
function escapeHtml(input) {
|
|
6361
6571
|
return (input ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
6362
6572
|
}
|
|
6363
|
-
function
|
|
6364
|
-
|
|
6365
|
-
}
|
|
6366
|
-
function toPrintableHtml(title, bodyMarkdownOrHtml) {
|
|
6367
|
-
const body = looksLikeHtml2(bodyMarkdownOrHtml) ? bodyMarkdownOrHtml : markdownToHtml(bodyMarkdownOrHtml);
|
|
6573
|
+
function toPrintableHtml(title, bodyMarkdown) {
|
|
6574
|
+
const body = markdownToHtml(bodyMarkdown);
|
|
6368
6575
|
const safeTitle = escapeHtml(title);
|
|
6369
6576
|
return `<!doctype html>
|
|
6370
6577
|
<html lang="ko">
|
|
@@ -6765,7 +6972,7 @@ function exportInterview(jobId, format) {
|
|
|
6765
6972
|
|
|
6766
6973
|
// apps/web/src/info.ts
|
|
6767
6974
|
var APP_NAME = "CareerMate";
|
|
6768
|
-
var APP_VERSION = "0.1.0";
|
|
6975
|
+
var APP_VERSION = true ? "0.1.1" : "0.0.0-dev";
|
|
6769
6976
|
var _port = 0;
|
|
6770
6977
|
function setServerPort(port) {
|
|
6771
6978
|
_port = port;
|
|
@@ -6781,41 +6988,22 @@ function getServerInfo() {
|
|
|
6781
6988
|
exports_dir: getExportsDir(),
|
|
6782
6989
|
backups_dir: getBackupsDir(),
|
|
6783
6990
|
node_version: process.version,
|
|
6784
|
-
counts:
|
|
6785
|
-
profile: profileRepo.get() ? 1 : 0,
|
|
6786
|
-
experiences: experienceRepo.list().length,
|
|
6787
|
-
projects: projectRepo.list().length,
|
|
6788
|
-
skills: skillRepo.list().length,
|
|
6789
|
-
documents: documentRepo.list().length,
|
|
6790
|
-
cover_letters: coverLetterRepo.list().length,
|
|
6791
|
-
jobs: jobRepo.list().length,
|
|
6792
|
-
applications: applicationRepo.list().length,
|
|
6793
|
-
interview_preps: interviewRepo.list().length
|
|
6794
|
-
}
|
|
6991
|
+
counts: getEntityCounts()
|
|
6795
6992
|
};
|
|
6796
6993
|
}
|
|
6797
6994
|
|
|
6798
6995
|
// apps/web/src/settings.ts
|
|
6799
6996
|
import fs4 from "node:fs";
|
|
6800
6997
|
import path4 from "node:path";
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
"documents",
|
|
6807
|
-
"cover_letters",
|
|
6808
|
-
"cover_letter_versions",
|
|
6809
|
-
"jobs",
|
|
6810
|
-
"fit_analyses",
|
|
6811
|
-
"applications",
|
|
6812
|
-
"interview_preps",
|
|
6813
|
-
"activities"
|
|
6814
|
-
];
|
|
6998
|
+
function listTables(db) {
|
|
6999
|
+
return db.prepare(
|
|
7000
|
+
`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name != '_meta' ORDER BY name`
|
|
7001
|
+
).all().map((r) => r.name);
|
|
7002
|
+
}
|
|
6815
7003
|
function exportAll() {
|
|
6816
7004
|
const db = getDb();
|
|
6817
7005
|
const tables = {};
|
|
6818
|
-
for (const t of
|
|
7006
|
+
for (const t of listTables(db)) {
|
|
6819
7007
|
tables[t] = db.prepare(`SELECT * FROM ${t}`).all();
|
|
6820
7008
|
}
|
|
6821
7009
|
const ver = db.prepare(`SELECT value FROM _meta WHERE key='schema_version'`).get();
|
|
@@ -6826,7 +7014,13 @@ function createBackup() {
|
|
|
6826
7014
|
const dir = getBackupsDir();
|
|
6827
7015
|
const dbBackup = path4.join(dir, `careermate-${stamp}.sqlite`);
|
|
6828
7016
|
const jsonBackup = path4.join(dir, `careermate-${stamp}.json`);
|
|
7017
|
+
const resolvedDir = path4.resolve(dir);
|
|
7018
|
+
if (!path4.resolve(dbBackup).startsWith(resolvedDir + path4.sep)) {
|
|
7019
|
+
throw new Error("\uBC31\uC5C5 \uACBD\uB85C\uAC00 \uBC31\uC5C5 \uD3F4\uB354\uB97C \uBC97\uC5B4\uB0AC\uC2B5\uB2C8\uB2E4.");
|
|
7020
|
+
}
|
|
7021
|
+
const safeForLiteral = !/[\0\n\r]/.test(dbBackup);
|
|
6829
7022
|
try {
|
|
7023
|
+
if (!safeForLiteral) throw new Error("backup path not safe for SQL literal");
|
|
6830
7024
|
getDb().exec(`VACUUM INTO '${dbBackup.replace(/'/g, "''")}'`);
|
|
6831
7025
|
} catch {
|
|
6832
7026
|
fs4.copyFileSync(getDbPath(), dbBackup);
|
|
@@ -6842,7 +7036,7 @@ function resetAll(confirm) {
|
|
|
6842
7036
|
const db = getDb();
|
|
6843
7037
|
db.exec("BEGIN");
|
|
6844
7038
|
try {
|
|
6845
|
-
for (const t of
|
|
7039
|
+
for (const t of listTables(db)) db.exec(`DELETE FROM ${t}`);
|
|
6846
7040
|
db.exec("COMMIT");
|
|
6847
7041
|
} catch (e) {
|
|
6848
7042
|
db.exec("ROLLBACK");
|
|
@@ -6891,9 +7085,11 @@ function registerApiRoutes(router2) {
|
|
|
6891
7085
|
router2.get("/api/health", () => ({ ok: true, ...getServerInfo() }));
|
|
6892
7086
|
router2.get("/api/onboarding", () => getOnboardingStatus());
|
|
6893
7087
|
router2.get("/api/summary", () => getHomeSummary());
|
|
6894
|
-
router2.get("/api/activity", (ctx) =>
|
|
6895
|
-
|
|
6896
|
-
|
|
7088
|
+
router2.get("/api/activity", (ctx) => {
|
|
7089
|
+
const raw = Number(ctx.query.get("limit"));
|
|
7090
|
+
const limit = Number.isFinite(raw) && raw > 0 ? Math.min(raw, 100) : 30;
|
|
7091
|
+
return { activities: listRecentActivity(limit) };
|
|
7092
|
+
});
|
|
6897
7093
|
router2.get(
|
|
6898
7094
|
"/api/context",
|
|
6899
7095
|
(ctx) => getApplicationContext({ job_id: ctx.query.get("job_id") ?? void 0 })
|
|
@@ -6912,9 +7108,7 @@ function registerApiRoutes(router2) {
|
|
|
6912
7108
|
router2.get("/api/experiences", () => ({ experiences: experienceRepo.list() }));
|
|
6913
7109
|
router2.post("/api/experiences", async (ctx) => {
|
|
6914
7110
|
const input = await readJsonBody(ctx.req, ExperienceInputSchema);
|
|
6915
|
-
|
|
6916
|
-
activityRepo.log("profile_updated", `\uACBD\uB825(${exp.company})\uC744 \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "experience", exp.id);
|
|
6917
|
-
return { experience: exp };
|
|
7111
|
+
return { experience: addExperience(input) };
|
|
6918
7112
|
});
|
|
6919
7113
|
router2.put("/api/experiences/:id", async (ctx) => {
|
|
6920
7114
|
const input = await readJsonBody(ctx.req, ExperienceInputSchema.partial());
|
|
@@ -6926,9 +7120,7 @@ function registerApiRoutes(router2) {
|
|
|
6926
7120
|
router2.get("/api/projects", () => ({ projects: projectRepo.list() }));
|
|
6927
7121
|
router2.post("/api/projects", async (ctx) => {
|
|
6928
7122
|
const input = await readJsonBody(ctx.req, ProjectInputSchema);
|
|
6929
|
-
|
|
6930
|
-
activityRepo.log("profile_updated", `\uD504\uB85C\uC81D\uD2B8(${prj.name})\uB97C \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "project", prj.id);
|
|
6931
|
-
return { project: prj };
|
|
7123
|
+
return { project: addProject(input) };
|
|
6932
7124
|
});
|
|
6933
7125
|
router2.put("/api/projects/:id", async (ctx) => {
|
|
6934
7126
|
const input = await readJsonBody(ctx.req, ProjectInputSchema.partial());
|
|
@@ -6940,7 +7132,7 @@ function registerApiRoutes(router2) {
|
|
|
6940
7132
|
router2.get("/api/skills", () => ({ skills: skillRepo.list() }));
|
|
6941
7133
|
router2.post("/api/skills", async (ctx) => {
|
|
6942
7134
|
const input = await readJsonBody(ctx.req, SkillInputSchema);
|
|
6943
|
-
return { skill:
|
|
7135
|
+
return { skill: addSkill(input) };
|
|
6944
7136
|
});
|
|
6945
7137
|
router2.put("/api/skills/:id", async (ctx) => {
|
|
6946
7138
|
const input = await readJsonBody(ctx.req, SkillInputSchema.partial());
|
|
@@ -7022,9 +7214,9 @@ function registerApiRoutes(router2) {
|
|
|
7022
7214
|
router2.get("/api/jobs/:id", (ctx) => ({ job: jobDetail(id2(ctx)) }));
|
|
7023
7215
|
router2.put("/api/jobs/:id", async (ctx) => {
|
|
7024
7216
|
const input = await readJsonBody(ctx.req, JobInputSchema.partial());
|
|
7025
|
-
const
|
|
7026
|
-
if (!
|
|
7027
|
-
const job = jobRepo.upsert(
|
|
7217
|
+
const jid = id2(ctx);
|
|
7218
|
+
if (!jobRepo.get(jid)) throw new HttpError(404, "\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
7219
|
+
const job = jobRepo.upsert(input, jid);
|
|
7028
7220
|
return { job: jobWithMeta(job) };
|
|
7029
7221
|
});
|
|
7030
7222
|
router2.delete("/api/jobs/:id", (ctx) => ({ ok: jobRepo.remove(id2(ctx)) }));
|
|
@@ -7153,8 +7345,8 @@ function createServer() {
|
|
|
7153
7345
|
}
|
|
7154
7346
|
if (pathname.startsWith("/install/")) {
|
|
7155
7347
|
const rel2 = pathname === "/install/" ? "index.html" : pathname.slice("/install/".length);
|
|
7156
|
-
if (serveStatic(res, INSTALL_ROOT, rel2
|
|
7157
|
-
if (serveStatic(res, INSTALL_ROOT, "index.html"
|
|
7348
|
+
if (serveStatic(res, INSTALL_ROOT, rel2)) return;
|
|
7349
|
+
if (serveStatic(res, INSTALL_ROOT, "index.html")) return;
|
|
7158
7350
|
}
|
|
7159
7351
|
const rel = pathname === "/" ? "index.html" : pathname.slice(1);
|
|
7160
7352
|
if (serveStatic(res, WEB_ROOT, rel, injectToken)) return;
|
|
@@ -7176,29 +7368,31 @@ function startServer(preferredPort) {
|
|
|
7176
7368
|
const server = createServer();
|
|
7177
7369
|
return new Promise((resolve, reject) => {
|
|
7178
7370
|
let attempt = 0;
|
|
7179
|
-
const
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
}
|
|
7187
|
-
});
|
|
7188
|
-
server.listen(port, "127.0.0.1", () => {
|
|
7189
|
-
setServerPort(port);
|
|
7190
|
-
const url = `http://127.0.0.1:${port}`;
|
|
7191
|
-
writeRuntimeInfo({ url, port, pid: process.pid, started_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
7192
|
-
const cleanup = () => {
|
|
7193
|
-
clearRuntimeInfo();
|
|
7194
|
-
process.exit(0);
|
|
7195
|
-
};
|
|
7196
|
-
process.on("SIGINT", cleanup);
|
|
7197
|
-
process.on("SIGTERM", cleanup);
|
|
7198
|
-
resolve({ server, port, url });
|
|
7199
|
-
});
|
|
7371
|
+
const onError = (e) => {
|
|
7372
|
+
if (e.code === "EADDRINUSE" && attempt < 15) {
|
|
7373
|
+
attempt++;
|
|
7374
|
+
server.listen(preferredPort + attempt, "127.0.0.1");
|
|
7375
|
+
} else {
|
|
7376
|
+
reject(e);
|
|
7377
|
+
}
|
|
7200
7378
|
};
|
|
7201
|
-
|
|
7379
|
+
server.on("error", onError);
|
|
7380
|
+
server.once("listening", () => {
|
|
7381
|
+
server.removeListener("error", onError);
|
|
7382
|
+
const addr = server.address();
|
|
7383
|
+
const port = typeof addr === "object" && addr ? addr.port : preferredPort;
|
|
7384
|
+
setServerPort(port);
|
|
7385
|
+
const url = `http://127.0.0.1:${port}`;
|
|
7386
|
+
writeRuntimeInfo({ url, port, pid: process.pid, started_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
7387
|
+
const cleanup = () => {
|
|
7388
|
+
clearRuntimeInfo();
|
|
7389
|
+
process.exit(0);
|
|
7390
|
+
};
|
|
7391
|
+
process.on("SIGINT", cleanup);
|
|
7392
|
+
process.on("SIGTERM", cleanup);
|
|
7393
|
+
resolve({ server, port, url });
|
|
7394
|
+
});
|
|
7395
|
+
server.listen(preferredPort, "127.0.0.1");
|
|
7202
7396
|
});
|
|
7203
7397
|
}
|
|
7204
7398
|
|
package/docs/DEMO.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
> 핵심 모델: **CareerMate 안에는 LLM이 없습니다.** 분석·작성은 당신의 AI(ChatGPT/Claude/Gemini)가 수행하고, CareerMate는 로컬 DB에 데이터를 읽고/쓰는 **MCP 도구**를 제공합니다. 모든 데이터는 로컬(`~/.careermate`)에만 저장됩니다.
|
|
9
9
|
>
|
|
10
|
-
> 관련 문서: [`INSTALL.md`](
|
|
10
|
+
> 관련 문서: [`INSTALL.md`](../INSTALL.md) · [`START_WORKFLOW.md`](./START_WORKFLOW.md)
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
@@ -106,7 +106,7 @@ npm start
|
|
|
106
106
|
|
|
107
107
|
### 5단계 — AI 클라이언트에서 MCP 연결 확인 🧑
|
|
108
108
|
|
|
109
|
-
- CareerMate는 **로컬 stdio MCP 서버**라, 내 컴퓨터에서 실행되며 로컬 프로그램을 띄울 수 있는 클라이언트에서만 동작합니다 — Claude Desktop · Cursor · Cline · Windsurf · Claude Code. (ChatGPT·Gemini의 웹/앱은 클라우드에서 돌아 로컬 MCP 서버에 직접 연결할 수 없습니다.) 설치는
|
|
109
|
+
- CareerMate는 **로컬 stdio MCP 서버**라, 내 컴퓨터에서 실행되며 로컬 프로그램을 띄울 수 있는 클라이언트에서만 동작합니다 — Claude Desktop · Cursor · Cline · Windsurf · Claude Code. (ChatGPT·Gemini의 웹/앱은 클라우드에서 돌아 로컬 MCP 서버에 직접 연결할 수 없습니다.) 설치는 크게 세 갈래입니다: 에이전트가 한 문장으로 대신 설치 / 터미널 `npx -y careermate init` / Claude Desktop `.mcpb` — 자세한 절차는 [`SUPPORTED_AI_APPS.md`](./SUPPORTED_AI_APPS.md)(또는 사람용 안내 https://careermate.life)를 참고하세요. MCP 서버 자체는 보통 다음 형태로 기동됩니다(클라이언트가 자동 실행).
|
|
110
110
|
|
|
111
111
|
```bash
|
|
112
112
|
npm run mcp
|
|
@@ -239,4 +239,4 @@ npm run mcp
|
|
|
239
239
|
7. `save_interview_prep` — 면접 준비 저장
|
|
240
240
|
8. `open_application` / `open_dashboard` — 사용자에게 결과 보여주기
|
|
241
241
|
|
|
242
|
-
> 더 자세한 단계별 런북은 [`START_WORKFLOW.md`](./START_WORKFLOW.md), 설치·연결은 [`INSTALL.md`](
|
|
242
|
+
> 더 자세한 단계별 런북은 [`START_WORKFLOW.md`](./START_WORKFLOW.md), 설치·연결은 [`INSTALL.md`](../INSTALL.md)를 참고하세요.
|