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.
Files changed (93) hide show
  1. package/README.md +10 -7
  2. package/apps/mcp/src/index.ts +3 -1
  3. package/apps/web/DESIGN_GUIDE.md +2 -2
  4. package/apps/web/public/icon-180.png +0 -0
  5. package/apps/web/public/index.html +4 -1
  6. package/apps/web/public/lib.js +1 -1
  7. package/apps/web/public/pages/applications.js +17 -1
  8. package/apps/web/public/pages/settings.js +6 -4
  9. package/apps/web/public/styles.css +49 -36
  10. package/apps/web/src/info.ts +9 -22
  11. package/apps/web/src/routes.ts +17 -13
  12. package/apps/web/src/server.ts +33 -25
  13. package/apps/web/src/settings.ts +35 -17
  14. package/bin/careermate.mjs +8 -5
  15. package/dist/{mcpb-stage/dist/web.mjs → doctor.mjs} +1122 -1450
  16. package/dist/install-page/favicon.svg +1 -0
  17. package/dist/install-page/icon-180.png +0 -0
  18. package/dist/install-page/icon-512.png +0 -0
  19. package/dist/install-page/index.html +309 -167
  20. package/dist/install-page/llms-install.txt +202 -0
  21. package/dist/install-page/og.png +0 -0
  22. package/dist/install-page/shots/applications-dark.png +0 -0
  23. package/dist/install-page/shots/applications-light.png +0 -0
  24. package/dist/install-page/shots/documents-dark.png +0 -0
  25. package/dist/install-page/shots/documents-light.png +0 -0
  26. package/dist/install-page/shots/home-dark.png +0 -0
  27. package/dist/install-page/shots/home-light.png +0 -0
  28. package/dist/install-page/shots/interview-dark.png +0 -0
  29. package/dist/install-page/shots/interview-light.png +0 -0
  30. package/dist/install-page/shots/jobs-dark.png +0 -0
  31. package/dist/install-page/shots/jobs-light.png +0 -0
  32. package/dist/install-page/shots/settings-dark.png +0 -0
  33. package/dist/install-page/shots/settings-light.png +0 -0
  34. package/dist/install-page/start.html +346 -0
  35. package/dist/install-page/style.css +94 -3
  36. package/dist/mcp.mjs +326 -68
  37. package/dist/migrate.mjs +4715 -0
  38. package/dist/public/icon-180.png +0 -0
  39. package/dist/public/index.html +4 -1
  40. package/dist/public/lib.js +1 -1
  41. package/dist/public/pages/applications.js +17 -1
  42. package/dist/public/pages/settings.js +6 -4
  43. package/dist/public/styles.css +49 -36
  44. package/dist/web.mjs +323 -129
  45. package/docs/DEMO.md +3 -3
  46. package/docs/MCP_TOOLS.md +21 -2
  47. package/docs/ROADMAP.md +3 -3
  48. package/docs/RUNTIME_DISTRIBUTION_RESEARCH.md +135 -0
  49. package/docs/START_WORKFLOW.md +6 -0
  50. package/docs/SUPPORTED_AI_APPS.md +85 -33
  51. package/docs/WORKFLOWS.md +1 -1
  52. package/package.json +3 -2
  53. package/packages/core/src/services.ts +42 -1
  54. package/packages/db/src/connection.ts +30 -0
  55. package/packages/db/src/index.ts +1 -0
  56. package/packages/db/src/repositories.ts +109 -63
  57. package/packages/db/src/schema.ts +118 -0
  58. package/packages/exporters/src/html.ts +9 -12
  59. package/packages/mcp-tools/src/tools.ts +81 -0
  60. package/packages/shared/src/enums.ts +39 -0
  61. package/scripts/build-dist.mjs +27 -8
  62. package/scripts/build-mcpb.mjs +60 -4
  63. package/scripts/gen-og.mjs +86 -0
  64. package/scripts/init.ts +32 -3
  65. package/scripts/seed.ts +5 -5
  66. package/scripts/shots-seed.ts +192 -0
  67. package/scripts/shots.ts +89 -0
  68. package/tsconfig.json +3 -1
  69. package/dist/careermate.mcpb +0 -0
  70. package/dist/mcp-smoke.err +0 -3
  71. package/dist/mcpb-stage/README.md +0 -219
  72. package/dist/mcpb-stage/dist/install-page/index.html +0 -434
  73. package/dist/mcpb-stage/dist/install-page/style.css +0 -407
  74. package/dist/mcpb-stage/dist/install-page/vercel.json +0 -20
  75. package/dist/mcpb-stage/dist/mcp.mjs +0 -23704
  76. package/dist/mcpb-stage/dist/public/app.js +0 -118
  77. package/dist/mcpb-stage/dist/public/index.html +0 -41
  78. package/dist/mcpb-stage/dist/public/lib.js +0 -282
  79. package/dist/mcpb-stage/dist/public/pages/applications.js +0 -98
  80. package/dist/mcpb-stage/dist/public/pages/documents.js +0 -446
  81. package/dist/mcpb-stage/dist/public/pages/home.js +0 -263
  82. package/dist/mcpb-stage/dist/public/pages/interview.js +0 -230
  83. package/dist/mcpb-stage/dist/public/pages/jobs.js +0 -494
  84. package/dist/mcpb-stage/dist/public/pages/profile.js +0 -576
  85. package/dist/mcpb-stage/dist/public/pages/settings.js +0 -233
  86. package/dist/mcpb-stage/dist/public/styles.css +0 -420
  87. package/dist/mcpb-stage/manifest.json +0 -40
  88. package/docs/INSTALL.md +0 -148
  89. package/docs/INSTALL_AND_USAGE.md +0 -99
  90. package/install-page/index.html +0 -474
  91. package/install-page/style.css +0 -391
  92. package/install-page/vercel.json +0 -20
  93. /package/dist/{mcpb-stage/dist/public → install-page}/fonts/PretendardVariable.woff2 +0 -0
@@ -5,13 +5,8 @@ var __export = (target, all) => {
5
5
  __defProp(target, name, { get: all[name], enumerable: true });
6
6
  };
7
7
 
8
- // apps/web/src/index.ts
9
- import { spawn } from "node:child_process";
10
-
11
- // apps/web/src/server.ts
12
- import http from "node:http";
13
- import path5 from "node:path";
14
- import { fileURLToPath } from "node:url";
8
+ // scripts/doctor.ts
9
+ import fs4 from "node:fs";
15
10
 
16
11
  // packages/db/src/paths.ts
17
12
  import os from "node:os";
@@ -31,11 +26,6 @@ function getExportsDir() {
31
26
  fs.mkdirSync(dir, { recursive: true });
32
27
  return dir;
33
28
  }
34
- function getBackupsDir() {
35
- const dir = path.join(getDataDir(), "backups");
36
- fs.mkdirSync(dir, { recursive: true });
37
- return dir;
38
- }
39
29
 
40
30
  // packages/db/src/runtime.ts
41
31
  import fs2 from "node:fs";
@@ -43,16 +33,20 @@ import path2 from "node:path";
43
33
  function runtimeFile() {
44
34
  return path2.join(getDataDir(), "server.json");
45
35
  }
46
- function writeRuntimeInfo(info) {
36
+ function readRuntimeInfo() {
47
37
  try {
48
- fs2.writeFileSync(runtimeFile(), JSON.stringify(info, null, 2), "utf8");
38
+ const raw = fs2.readFileSync(runtimeFile(), "utf8");
39
+ return JSON.parse(raw);
49
40
  } catch {
41
+ return null;
50
42
  }
51
43
  }
52
- function clearRuntimeInfo() {
44
+ function isProcessAlive(pid) {
53
45
  try {
54
- fs2.unlinkSync(runtimeFile());
55
- } catch {
46
+ process.kill(pid, 0);
47
+ return true;
48
+ } catch (e) {
49
+ return e.code === "EPERM";
56
50
  }
57
51
  }
58
52
 
@@ -216,6 +210,123 @@ var MIGRATIONS = [
216
210
  CREATE INDEX IF NOT EXISTS idx_clv_letter ON cover_letter_versions(cover_letter_id);
217
211
  CREATE INDEX IF NOT EXISTS idx_cl_job ON cover_letters(job_id);
218
212
  CREATE INDEX IF NOT EXISTS idx_activities_created ON activities(created_at DESC);
213
+ `,
214
+ // v2 — add foreign keys so PRAGMA foreign_keys=ON is actually enforced.
215
+ //
216
+ // SQLite cannot ALTER a table to add constraints, so each table that needs an
217
+ // FK is recreated (create new → copy → drop old → rename). The copy also
218
+ // repairs pre-existing orphans: child rows whose parent is gone are dropped
219
+ // (CASCADE intent) and dangling optional refs are nulled (SET NULL intent), so
220
+ // a subsequent PRAGMA foreign_key_check stays clean. FKs are disabled for the
221
+ // rebuild and the whole thing runs in one transaction (auto-rolled-back if the
222
+ // process dies mid-migration, since the version bump happens only afterwards).
223
+ `
224
+ PRAGMA foreign_keys=OFF;
225
+ BEGIN;
226
+
227
+ -- cover_letters.job_id -> jobs(id) ON DELETE SET NULL
228
+ CREATE TABLE cover_letters_new (
229
+ id TEXT PRIMARY KEY,
230
+ title TEXT NOT NULL,
231
+ job_id TEXT REFERENCES jobs(id) ON DELETE SET NULL,
232
+ is_primary INTEGER NOT NULL DEFAULT 0,
233
+ current_version_id TEXT,
234
+ created_at TEXT NOT NULL,
235
+ updated_at TEXT NOT NULL
236
+ );
237
+ INSERT INTO cover_letters_new (id,title,job_id,is_primary,current_version_id,created_at,updated_at)
238
+ SELECT id,title,
239
+ CASE WHEN job_id IN (SELECT id FROM jobs) THEN job_id ELSE NULL END,
240
+ is_primary,current_version_id,created_at,updated_at
241
+ FROM cover_letters;
242
+ DROP TABLE cover_letters;
243
+ ALTER TABLE cover_letters_new RENAME TO cover_letters;
244
+
245
+ -- cover_letter_versions.cover_letter_id -> cover_letters(id) ON DELETE CASCADE
246
+ CREATE TABLE cover_letter_versions_new (
247
+ id TEXT PRIMARY KEY,
248
+ cover_letter_id TEXT NOT NULL REFERENCES cover_letters(id) ON DELETE CASCADE,
249
+ version_no INTEGER NOT NULL,
250
+ content TEXT NOT NULL,
251
+ note TEXT,
252
+ source TEXT NOT NULL DEFAULT 'ai',
253
+ created_at TEXT NOT NULL
254
+ );
255
+ INSERT INTO cover_letter_versions_new (id,cover_letter_id,version_no,content,note,source,created_at)
256
+ SELECT id,cover_letter_id,version_no,content,note,source,created_at
257
+ FROM cover_letter_versions
258
+ WHERE cover_letter_id IN (SELECT id FROM cover_letters);
259
+ DROP TABLE cover_letter_versions;
260
+ ALTER TABLE cover_letter_versions_new RENAME TO cover_letter_versions;
261
+
262
+ -- fit_analyses.job_id -> jobs(id) ON DELETE CASCADE
263
+ CREATE TABLE fit_analyses_new (
264
+ id TEXT PRIMARY KEY,
265
+ job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
266
+ score REAL,
267
+ summary TEXT,
268
+ strengths TEXT NOT NULL DEFAULT '[]',
269
+ gaps TEXT NOT NULL DEFAULT '[]',
270
+ matched_keywords TEXT NOT NULL DEFAULT '[]',
271
+ missing_keywords TEXT NOT NULL DEFAULT '[]',
272
+ recommendations TEXT NOT NULL DEFAULT '[]',
273
+ created_at TEXT NOT NULL,
274
+ updated_at TEXT NOT NULL
275
+ );
276
+ INSERT INTO fit_analyses_new (id,job_id,score,summary,strengths,gaps,matched_keywords,missing_keywords,recommendations,created_at,updated_at)
277
+ SELECT id,job_id,score,summary,strengths,gaps,matched_keywords,missing_keywords,recommendations,created_at,updated_at
278
+ FROM fit_analyses
279
+ WHERE job_id IN (SELECT id FROM jobs);
280
+ DROP TABLE fit_analyses;
281
+ ALTER TABLE fit_analyses_new RENAME TO fit_analyses;
282
+
283
+ -- applications: job_id CASCADE, resume_id/cover_letter_id SET NULL
284
+ CREATE TABLE applications_new (
285
+ id TEXT PRIMARY KEY,
286
+ job_id TEXT NOT NULL UNIQUE REFERENCES jobs(id) ON DELETE CASCADE,
287
+ status TEXT NOT NULL DEFAULT 'draft',
288
+ resume_id TEXT REFERENCES documents(id) ON DELETE SET NULL,
289
+ cover_letter_id TEXT REFERENCES cover_letters(id) ON DELETE SET NULL,
290
+ applied_at TEXT,
291
+ notes TEXT,
292
+ created_at TEXT NOT NULL,
293
+ updated_at TEXT NOT NULL
294
+ );
295
+ INSERT INTO applications_new (id,job_id,status,resume_id,cover_letter_id,applied_at,notes,created_at,updated_at)
296
+ SELECT id,job_id,status,
297
+ CASE WHEN resume_id IN (SELECT id FROM documents) THEN resume_id ELSE NULL END,
298
+ CASE WHEN cover_letter_id IN (SELECT id FROM cover_letters) THEN cover_letter_id ELSE NULL END,
299
+ applied_at,notes,created_at,updated_at
300
+ FROM applications
301
+ WHERE job_id IN (SELECT id FROM jobs);
302
+ DROP TABLE applications;
303
+ ALTER TABLE applications_new RENAME TO applications;
304
+
305
+ -- interview_preps.job_id -> jobs(id) ON DELETE CASCADE
306
+ CREATE TABLE interview_preps_new (
307
+ id TEXT PRIMARY KEY,
308
+ job_id TEXT NOT NULL UNIQUE REFERENCES jobs(id) ON DELETE CASCADE,
309
+ questions TEXT NOT NULL DEFAULT '[]',
310
+ star_guides TEXT NOT NULL DEFAULT '[]',
311
+ self_introduction TEXT,
312
+ notes TEXT,
313
+ created_at TEXT NOT NULL,
314
+ updated_at TEXT NOT NULL
315
+ );
316
+ INSERT INTO interview_preps_new (id,job_id,questions,star_guides,self_introduction,notes,created_at,updated_at)
317
+ SELECT id,job_id,questions,star_guides,self_introduction,notes,created_at,updated_at
318
+ FROM interview_preps
319
+ WHERE job_id IN (SELECT id FROM jobs);
320
+ DROP TABLE interview_preps;
321
+ ALTER TABLE interview_preps_new RENAME TO interview_preps;
322
+
323
+ -- Recreate indexes dropped along with their tables.
324
+ CREATE INDEX IF NOT EXISTS idx_fit_job ON fit_analyses(job_id);
325
+ CREATE INDEX IF NOT EXISTS idx_clv_letter ON cover_letter_versions(cover_letter_id);
326
+ CREATE INDEX IF NOT EXISTS idx_cl_job ON cover_letters(job_id);
327
+
328
+ COMMIT;
329
+ PRAGMA foreign_keys=ON;
219
330
  `
220
331
  ];
221
332
  function migrate(db) {
@@ -247,6 +358,26 @@ function getDb() {
247
358
  _db = db;
248
359
  return db;
249
360
  }
361
+ var _inTx = false;
362
+ function tx(fn) {
363
+ if (_inTx) return fn();
364
+ const db = getDb();
365
+ db.exec("BEGIN");
366
+ _inTx = true;
367
+ try {
368
+ const result = fn();
369
+ db.exec("COMMIT");
370
+ return result;
371
+ } catch (err) {
372
+ try {
373
+ db.exec("ROLLBACK");
374
+ } catch {
375
+ }
376
+ throw err;
377
+ } finally {
378
+ _inTx = false;
379
+ }
380
+ }
250
381
  function toJson(value) {
251
382
  return JSON.stringify(value ?? null);
252
383
  }
@@ -290,28 +421,33 @@ var APPLICATION_STATUS_LABELS = {
290
421
  rejected: "\uBD88\uD569\uACA9",
291
422
  on_hold: "\uBCF4\uB958"
292
423
  };
293
- var APPLICATION_BOARD_ORDER = [
294
- "draft",
295
- "planned",
296
- "applied",
297
- "document_passed",
298
- "interview",
299
- "final_passed",
300
- "on_hold",
301
- "rejected"
302
- ];
303
424
  var INTERVIEW_UNLOCK_STATUSES = [
304
425
  "document_passed",
305
426
  "interview",
306
427
  "final_passed"
307
428
  ];
308
- var DOCUMENT_KINDS = ["resume", "career_description", "portfolio", "other"];
309
- var DOCUMENT_KIND_LABELS = {
310
- resume: "\uC774\uB825\uC11C",
311
- career_description: "\uACBD\uB825\uAE30\uC220\uC11C",
312
- portfolio: "\uD3EC\uD2B8\uD3F4\uB9AC\uC624",
313
- other: "\uAE30\uD0C0 \uBB38\uC11C"
429
+ var ALLOWED_STATUS_TRANSITIONS = {
430
+ draft: ["planned", "applied", "document_passed", "interview", "final_passed", "rejected", "on_hold"],
431
+ planned: ["applied", "document_passed", "interview", "final_passed", "rejected", "on_hold"],
432
+ applied: ["planned", "document_passed", "interview", "final_passed", "rejected", "on_hold"],
433
+ document_passed: ["planned", "applied", "interview", "final_passed", "rejected", "on_hold"],
434
+ interview: ["planned", "applied", "document_passed", "final_passed", "rejected", "on_hold"],
435
+ final_passed: ["document_passed", "interview", "rejected", "on_hold"],
436
+ rejected: ["draft", "planned", "applied", "document_passed", "interview", "final_passed", "on_hold"],
437
+ on_hold: ["draft", "planned", "applied", "document_passed", "interview", "final_passed", "rejected"]
314
438
  };
439
+ function canTransitionStatus(from, to) {
440
+ if (from === to) return true;
441
+ return ALLOWED_STATUS_TRANSITIONS[from]?.includes(to) ?? false;
442
+ }
443
+ function assertStatusTransition(from, to) {
444
+ if (!canTransitionStatus(from, to)) {
445
+ throw new Error(
446
+ `'${APPLICATION_STATUS_LABELS[from]}'\uC5D0\uC11C '${APPLICATION_STATUS_LABELS[to]}'(\uC73C)\uB85C\uB294 \uC0C1\uD0DC\uB97C \uBC14\uAFC0 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`
447
+ );
448
+ }
449
+ }
450
+ var DOCUMENT_KINDS = ["resume", "career_description", "portfolio", "other"];
315
451
  var CONTENT_SOURCES = ["manual", "upload", "ai", "edit"];
316
452
  var ACTIVITY_TYPES = [
317
453
  "profile_updated",
@@ -815,8 +951,8 @@ function getErrorMap() {
815
951
 
816
952
  // node_modules/zod/v3/helpers/parseUtil.js
817
953
  var makeIssue = (params) => {
818
- const { data, path: path6, errorMaps, issueData } = params;
819
- const fullPath = [...path6, ...issueData.path || []];
954
+ const { data, path: path5, errorMaps, issueData } = params;
955
+ const fullPath = [...path5, ...issueData.path || []];
820
956
  const fullIssue = {
821
957
  ...issueData,
822
958
  path: fullPath
@@ -932,11 +1068,11 @@ var errorUtil;
932
1068
 
933
1069
  // node_modules/zod/v3/types.js
934
1070
  var ParseInputLazyPath = class {
935
- constructor(parent, value, path6, key) {
1071
+ constructor(parent, value, path5, key) {
936
1072
  this._cachedPath = [];
937
1073
  this.parent = parent;
938
1074
  this.data = value;
939
- this._path = path6;
1075
+ this._path = path5;
940
1076
  this._key = key;
941
1077
  }
942
1078
  get path() {
@@ -4766,18 +4902,18 @@ var experienceRepo = {
4766
4902
  list() {
4767
4903
  return getDb().prepare(`SELECT * FROM experiences ORDER BY order_index ASC, start_date DESC`).all().map(mapExperience);
4768
4904
  },
4769
- get(id3) {
4770
- const r = getDb().prepare(`SELECT * FROM experiences WHERE id = ?`).get(id3);
4905
+ get(id2) {
4906
+ const r = getDb().prepare(`SELECT * FROM experiences WHERE id = ?`).get(id2);
4771
4907
  return r ? mapExperience(r) : null;
4772
4908
  },
4773
4909
  add(input) {
4774
4910
  const ts = now();
4775
- const id3 = newId("exp_");
4911
+ const id2 = newId("exp_");
4776
4912
  getDb().prepare(
4777
4913
  `INSERT INTO experiences (id,company,role,employment_type,start_date,end_date,is_current,description,achievements,tech,order_index,created_at,updated_at)
4778
4914
  VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`
4779
4915
  ).run(
4780
- id3,
4916
+ id2,
4781
4917
  input.company,
4782
4918
  input.role ?? null,
4783
4919
  input.employment_type ?? null,
@@ -4791,10 +4927,10 @@ var experienceRepo = {
4791
4927
  ts,
4792
4928
  ts
4793
4929
  );
4794
- return this.get(id3);
4930
+ return this.get(id2);
4795
4931
  },
4796
- update(id3, input) {
4797
- const cur = this.get(id3);
4932
+ update(id2, input) {
4933
+ const cur = this.get(id2);
4798
4934
  if (!cur) return null;
4799
4935
  const m = { ...cur, ...input };
4800
4936
  getDb().prepare(
@@ -4811,12 +4947,12 @@ var experienceRepo = {
4811
4947
  toJson(m.tech),
4812
4948
  m.order_index,
4813
4949
  now(),
4814
- id3
4950
+ id2
4815
4951
  );
4816
- return this.get(id3);
4952
+ return this.get(id2);
4817
4953
  },
4818
- remove(id3) {
4819
- return getDb().prepare(`DELETE FROM experiences WHERE id=?`).run(id3).changes > 0;
4954
+ remove(id2) {
4955
+ return getDb().prepare(`DELETE FROM experiences WHERE id=?`).run(id2).changes > 0;
4820
4956
  }
4821
4957
  };
4822
4958
  function mapProject(r) {
@@ -4839,18 +4975,18 @@ var projectRepo = {
4839
4975
  list() {
4840
4976
  return getDb().prepare(`SELECT * FROM projects ORDER BY order_index ASC, start_date DESC`).all().map(mapProject);
4841
4977
  },
4842
- get(id3) {
4843
- const r = getDb().prepare(`SELECT * FROM projects WHERE id = ?`).get(id3);
4978
+ get(id2) {
4979
+ const r = getDb().prepare(`SELECT * FROM projects WHERE id = ?`).get(id2);
4844
4980
  return r ? mapProject(r) : null;
4845
4981
  },
4846
4982
  add(input) {
4847
4983
  const ts = now();
4848
- const id3 = newId("prj_");
4984
+ const id2 = newId("prj_");
4849
4985
  getDb().prepare(
4850
4986
  `INSERT INTO projects (id,name,role,description,highlights,tech,url,start_date,end_date,order_index,created_at,updated_at)
4851
4987
  VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`
4852
4988
  ).run(
4853
- id3,
4989
+ id2,
4854
4990
  input.name,
4855
4991
  input.role ?? null,
4856
4992
  input.description ?? null,
@@ -4863,10 +4999,10 @@ var projectRepo = {
4863
4999
  ts,
4864
5000
  ts
4865
5001
  );
4866
- return this.get(id3);
5002
+ return this.get(id2);
4867
5003
  },
4868
- update(id3, input) {
4869
- const cur = this.get(id3);
5004
+ update(id2, input) {
5005
+ const cur = this.get(id2);
4870
5006
  if (!cur) return null;
4871
5007
  const m = { ...cur, ...input };
4872
5008
  getDb().prepare(
@@ -4882,12 +5018,12 @@ var projectRepo = {
4882
5018
  m.end_date,
4883
5019
  m.order_index,
4884
5020
  now(),
4885
- id3
5021
+ id2
4886
5022
  );
4887
- return this.get(id3);
5023
+ return this.get(id2);
4888
5024
  },
4889
- remove(id3) {
4890
- return getDb().prepare(`DELETE FROM projects WHERE id=?`).run(id3).changes > 0;
5025
+ remove(id2) {
5026
+ return getDb().prepare(`DELETE FROM projects WHERE id=?`).run(id2).changes > 0;
4891
5027
  }
4892
5028
  };
4893
5029
  function mapSkill(r) {
@@ -4906,17 +5042,17 @@ var skillRepo = {
4906
5042
  list() {
4907
5043
  return getDb().prepare(`SELECT * FROM skills ORDER BY order_index ASC, name ASC`).all().map(mapSkill);
4908
5044
  },
4909
- get(id3) {
4910
- const r = getDb().prepare(`SELECT * FROM skills WHERE id = ?`).get(id3);
5045
+ get(id2) {
5046
+ const r = getDb().prepare(`SELECT * FROM skills WHERE id = ?`).get(id2);
4911
5047
  return r ? mapSkill(r) : null;
4912
5048
  },
4913
5049
  add(input) {
4914
5050
  const ts = now();
4915
- const id3 = newId("skl_");
5051
+ const id2 = newId("skl_");
4916
5052
  getDb().prepare(
4917
5053
  `INSERT INTO skills (id,name,category,level,years,order_index,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?)`
4918
5054
  ).run(
4919
- id3,
5055
+ id2,
4920
5056
  input.name,
4921
5057
  input.category ?? null,
4922
5058
  input.level ?? null,
@@ -4925,17 +5061,17 @@ var skillRepo = {
4925
5061
  ts,
4926
5062
  ts
4927
5063
  );
4928
- return this.get(id3);
5064
+ return this.get(id2);
4929
5065
  },
4930
- update(id3, input) {
4931
- const cur = this.get(id3);
5066
+ update(id2, input) {
5067
+ const cur = this.get(id2);
4932
5068
  if (!cur) return null;
4933
5069
  const m = { ...cur, ...input };
4934
- getDb().prepare(`UPDATE skills SET name=?,category=?,level=?,years=?,order_index=?,updated_at=? WHERE id=?`).run(m.name, m.category, m.level, m.years, m.order_index, now(), id3);
4935
- return this.get(id3);
5070
+ getDb().prepare(`UPDATE skills SET name=?,category=?,level=?,years=?,order_index=?,updated_at=? WHERE id=?`).run(m.name, m.category, m.level, m.years, m.order_index, now(), id2);
5071
+ return this.get(id2);
4936
5072
  },
4937
- remove(id3) {
4938
- return getDb().prepare(`DELETE FROM skills WHERE id=?`).run(id3).changes > 0;
5073
+ remove(id2) {
5074
+ return getDb().prepare(`DELETE FROM skills WHERE id=?`).run(id2).changes > 0;
4939
5075
  }
4940
5076
  };
4941
5077
  function mapDocument(r) {
@@ -4957,8 +5093,8 @@ var documentRepo = {
4957
5093
  const rows = kind ? db.prepare(`SELECT * FROM documents WHERE kind=? ORDER BY is_primary DESC, updated_at DESC`).all(kind) : db.prepare(`SELECT * FROM documents ORDER BY is_primary DESC, updated_at DESC`).all();
4958
5094
  return rows.map(mapDocument);
4959
5095
  },
4960
- get(id3) {
4961
- const r = getDb().prepare(`SELECT * FROM documents WHERE id=?`).get(id3);
5096
+ get(id2) {
5097
+ const r = getDb().prepare(`SELECT * FROM documents WHERE id=?`).get(id2);
4962
5098
  return r ? mapDocument(r) : null;
4963
5099
  },
4964
5100
  primary(kind) {
@@ -4967,39 +5103,43 @@ var documentRepo = {
4967
5103
  },
4968
5104
  add(input) {
4969
5105
  const ts = now();
4970
- const id3 = newId("doc_");
5106
+ const id2 = newId("doc_");
4971
5107
  const db = getDb();
4972
- if (input.is_primary) {
4973
- db.prepare(`UPDATE documents SET is_primary=0 WHERE kind=?`).run(input.kind);
4974
- }
4975
- db.prepare(
4976
- `INSERT INTO documents (id,kind,title,content,source,is_primary,tags,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?)`
4977
- ).run(
4978
- id3,
4979
- input.kind,
4980
- input.title,
4981
- input.content ?? "",
4982
- input.source ?? "manual",
4983
- toBit(input.is_primary),
4984
- toJson(input.tags ?? []),
4985
- ts,
4986
- ts
4987
- );
4988
- return this.get(id3);
5108
+ return tx(() => {
5109
+ if (input.is_primary) {
5110
+ db.prepare(`UPDATE documents SET is_primary=0 WHERE kind=?`).run(input.kind);
5111
+ }
5112
+ db.prepare(
5113
+ `INSERT INTO documents (id,kind,title,content,source,is_primary,tags,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?)`
5114
+ ).run(
5115
+ id2,
5116
+ input.kind,
5117
+ input.title,
5118
+ input.content ?? "",
5119
+ input.source ?? "manual",
5120
+ toBit(input.is_primary),
5121
+ toJson(input.tags ?? []),
5122
+ ts,
5123
+ ts
5124
+ );
5125
+ return this.get(id2);
5126
+ });
4989
5127
  },
4990
- update(id3, input) {
4991
- const cur = this.get(id3);
5128
+ update(id2, input) {
5129
+ const cur = this.get(id2);
4992
5130
  if (!cur) return null;
4993
5131
  const db = getDb();
4994
5132
  const m = { ...cur, ...input };
4995
- if (input.is_primary) db.prepare(`UPDATE documents SET is_primary=0 WHERE kind=?`).run(m.kind);
4996
- db.prepare(
4997
- `UPDATE documents SET kind=?,title=?,content=?,source=?,is_primary=?,tags=?,updated_at=? WHERE id=?`
4998
- ).run(m.kind, m.title, m.content, m.source, toBit(m.is_primary), toJson(m.tags), now(), id3);
4999
- return this.get(id3);
5133
+ return tx(() => {
5134
+ if (input.is_primary) db.prepare(`UPDATE documents SET is_primary=0 WHERE kind=?`).run(m.kind);
5135
+ db.prepare(
5136
+ `UPDATE documents SET kind=?,title=?,content=?,source=?,is_primary=?,tags=?,updated_at=? WHERE id=?`
5137
+ ).run(m.kind, m.title, m.content, m.source, toBit(m.is_primary), toJson(m.tags), now(), id2);
5138
+ return this.get(id2);
5139
+ });
5000
5140
  },
5001
- remove(id3) {
5002
- return getDb().prepare(`DELETE FROM documents WHERE id=?`).run(id3).changes > 0;
5141
+ remove(id2) {
5142
+ return getDb().prepare(`DELETE FROM documents WHERE id=?`).run(id2).changes > 0;
5003
5143
  }
5004
5144
  };
5005
5145
  function mapVersion(r) {
@@ -5034,8 +5174,8 @@ var coverLetterRepo = {
5034
5174
  list() {
5035
5175
  return getDb().prepare(`SELECT * FROM cover_letters ORDER BY is_primary DESC, updated_at DESC`).all().map((r) => mapCoverLetter(r, false));
5036
5176
  },
5037
- get(id3, withVersions = true) {
5038
- const r = getDb().prepare(`SELECT * FROM cover_letters WHERE id=?`).get(id3);
5177
+ get(id2, withVersions = true) {
5178
+ const r = getDb().prepare(`SELECT * FROM cover_letters WHERE id=?`).get(id2);
5039
5179
  return r ? mapCoverLetter(r, withVersions) : null;
5040
5180
  },
5041
5181
  listByJob(jobId) {
@@ -5043,54 +5183,62 @@ var coverLetterRepo = {
5043
5183
  },
5044
5184
  create(opts) {
5045
5185
  const ts = now();
5046
- const id3 = newId("cl_");
5186
+ const id2 = newId("cl_");
5047
5187
  const db = getDb();
5048
- if (opts.is_primary) db.prepare(`UPDATE cover_letters SET is_primary=0`).run();
5049
- db.prepare(
5050
- `INSERT INTO cover_letters (id,title,job_id,is_primary,current_version_id,created_at,updated_at) VALUES (?,?,?,?,?,?,?)`
5051
- ).run(id3, opts.title, opts.job_id ?? null, toBit(opts.is_primary), null, ts, ts);
5052
- return this.get(id3);
5188
+ return tx(() => {
5189
+ if (opts.is_primary) db.prepare(`UPDATE cover_letters SET is_primary=0`).run();
5190
+ db.prepare(
5191
+ `INSERT INTO cover_letters (id,title,job_id,is_primary,current_version_id,created_at,updated_at) VALUES (?,?,?,?,?,?,?)`
5192
+ ).run(id2, opts.title, opts.job_id ?? null, toBit(opts.is_primary), null, ts, ts);
5193
+ return this.get(id2);
5194
+ });
5053
5195
  },
5054
5196
  /** Append a new version. Creates the cover letter if cover_letter_id is omitted. */
5055
5197
  addVersion(opts) {
5056
5198
  const db = getDb();
5057
- let clId = opts.cover_letter_id;
5058
- if (!clId) {
5059
- const cl = this.create({ title: opts.title ?? "\uC790\uAE30\uC18C\uAC1C\uC11C", job_id: opts.job_id ?? null });
5060
- clId = cl.id;
5061
- }
5062
- const maxRow = db.prepare(`SELECT MAX(version_no) AS m FROM cover_letter_versions WHERE cover_letter_id=?`).get(clId);
5063
- const versionNo = (maxRow.m ?? 0) + 1;
5064
- const vid = newId("clv_");
5065
- const ts = now();
5066
- db.prepare(
5067
- `INSERT INTO cover_letter_versions (id,cover_letter_id,version_no,content,note,source,created_at) VALUES (?,?,?,?,?,?,?)`
5068
- ).run(vid, clId, versionNo, opts.content, opts.note ?? null, opts.source ?? "ai", ts);
5069
- const setCurrent = opts.set_current !== false;
5070
- if (setCurrent) {
5071
- db.prepare(`UPDATE cover_letters SET current_version_id=?, updated_at=? WHERE id=?`).run(vid, ts, clId);
5072
- } else {
5073
- db.prepare(`UPDATE cover_letters SET updated_at=? WHERE id=?`).run(ts, clId);
5074
- }
5075
- if (opts.title) db.prepare(`UPDATE cover_letters SET title=? WHERE id=?`).run(opts.title, clId);
5076
- if (opts.job_id !== void 0)
5077
- db.prepare(`UPDATE cover_letters SET job_id=? WHERE id=?`).run(opts.job_id, clId);
5078
- return { coverLetter: this.get(clId), version: mapVersion(db.prepare(`SELECT * FROM cover_letter_versions WHERE id=?`).get(vid)) };
5199
+ return tx(() => {
5200
+ let clId = opts.cover_letter_id;
5201
+ if (!clId) {
5202
+ const cl = this.create({ title: opts.title ?? "\uC790\uAE30\uC18C\uAC1C\uC11C", job_id: opts.job_id ?? null });
5203
+ clId = cl.id;
5204
+ }
5205
+ const maxRow = db.prepare(`SELECT MAX(version_no) AS m FROM cover_letter_versions WHERE cover_letter_id=?`).get(clId);
5206
+ const versionNo = (maxRow.m ?? 0) + 1;
5207
+ const vid = newId("clv_");
5208
+ const ts = now();
5209
+ db.prepare(
5210
+ `INSERT INTO cover_letter_versions (id,cover_letter_id,version_no,content,note,source,created_at) VALUES (?,?,?,?,?,?,?)`
5211
+ ).run(vid, clId, versionNo, opts.content, opts.note ?? null, opts.source ?? "ai", ts);
5212
+ const setCurrent = opts.set_current !== false;
5213
+ if (setCurrent) {
5214
+ db.prepare(`UPDATE cover_letters SET current_version_id=?, updated_at=? WHERE id=?`).run(vid, ts, clId);
5215
+ } else {
5216
+ db.prepare(`UPDATE cover_letters SET updated_at=? WHERE id=?`).run(ts, clId);
5217
+ }
5218
+ if (opts.title) db.prepare(`UPDATE cover_letters SET title=? WHERE id=?`).run(opts.title, clId);
5219
+ if (opts.job_id !== void 0)
5220
+ db.prepare(`UPDATE cover_letters SET job_id=? WHERE id=?`).run(opts.job_id, clId);
5221
+ return { coverLetter: this.get(clId), version: mapVersion(db.prepare(`SELECT * FROM cover_letter_versions WHERE id=?`).get(vid)) };
5222
+ });
5079
5223
  },
5080
5224
  setCurrentVersion(coverLetterId, versionId) {
5081
5225
  getDb().prepare(`UPDATE cover_letters SET current_version_id=?, updated_at=? WHERE id=?`).run(versionId, now(), coverLetterId);
5082
5226
  return this.get(coverLetterId);
5083
5227
  },
5084
- setPrimary(id3) {
5228
+ setPrimary(id2) {
5085
5229
  const db = getDb();
5086
- db.prepare(`UPDATE cover_letters SET is_primary=0`).run();
5087
- db.prepare(`UPDATE cover_letters SET is_primary=1, updated_at=? WHERE id=?`).run(now(), id3);
5088
- return this.get(id3);
5230
+ return tx(() => {
5231
+ db.prepare(`UPDATE cover_letters SET is_primary=0`).run();
5232
+ db.prepare(`UPDATE cover_letters SET is_primary=1, updated_at=? WHERE id=?`).run(now(), id2);
5233
+ return this.get(id2);
5234
+ });
5089
5235
  },
5090
- remove(id3) {
5236
+ remove(id2) {
5091
5237
  const db = getDb();
5092
- db.prepare(`DELETE FROM cover_letter_versions WHERE cover_letter_id=?`).run(id3);
5093
- return db.prepare(`DELETE FROM cover_letters WHERE id=?`).run(id3).changes > 0;
5238
+ return tx(() => {
5239
+ db.prepare(`DELETE FROM cover_letter_versions WHERE cover_letter_id=?`).run(id2);
5240
+ return db.prepare(`DELETE FROM cover_letters WHERE id=?`).run(id2).changes > 0;
5241
+ });
5094
5242
  }
5095
5243
  };
5096
5244
  function mapJob(r) {
@@ -5114,8 +5262,8 @@ var jobRepo = {
5114
5262
  list() {
5115
5263
  return getDb().prepare(`SELECT * FROM jobs ORDER BY updated_at DESC`).all().map(mapJob);
5116
5264
  },
5117
- get(id3) {
5118
- const r = getDb().prepare(`SELECT * FROM jobs WHERE id=?`).get(id3);
5265
+ get(id2) {
5266
+ const r = getDb().prepare(`SELECT * FROM jobs WHERE id=?`).get(id2);
5119
5267
  return r ? mapJob(r) : null;
5120
5268
  },
5121
5269
  findByUrl(url) {
@@ -5128,9 +5276,9 @@ var jobRepo = {
5128
5276
  ).all(company, `%${position}%`, excludeId ?? "");
5129
5277
  return rows.map(mapJob);
5130
5278
  },
5131
- upsert(input, id3) {
5279
+ upsert(input, id2) {
5132
5280
  const db = getDb();
5133
- const existing = id3 ? this.get(id3) : input.url ? this.findByUrl(input.url) : null;
5281
+ const existing = id2 ? this.get(id2) : input.url ? this.findByUrl(input.url) : null;
5134
5282
  const ts = now();
5135
5283
  if (existing) {
5136
5284
  const m = { ...existing, ...input };
@@ -5152,7 +5300,7 @@ var jobRepo = {
5152
5300
  );
5153
5301
  return this.get(existing.id);
5154
5302
  }
5155
- const newJobId = id3 ?? newId("job_");
5303
+ const newJobId = id2 ?? newId("job_");
5156
5304
  db.prepare(
5157
5305
  `INSERT INTO jobs (id,company,position,url,location,employment_type,description,requirements,keywords,deadline,source,created_at,updated_at)
5158
5306
  VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`
@@ -5173,12 +5321,14 @@ var jobRepo = {
5173
5321
  );
5174
5322
  return this.get(newJobId);
5175
5323
  },
5176
- remove(id3) {
5324
+ remove(id2) {
5177
5325
  const db = getDb();
5178
- db.prepare(`DELETE FROM fit_analyses WHERE job_id=?`).run(id3);
5179
- db.prepare(`DELETE FROM applications WHERE job_id=?`).run(id3);
5180
- db.prepare(`DELETE FROM interview_preps WHERE job_id=?`).run(id3);
5181
- return db.prepare(`DELETE FROM jobs WHERE id=?`).run(id3).changes > 0;
5326
+ return tx(() => {
5327
+ db.prepare(`DELETE FROM fit_analyses WHERE job_id=?`).run(id2);
5328
+ db.prepare(`DELETE FROM applications WHERE job_id=?`).run(id2);
5329
+ db.prepare(`DELETE FROM interview_preps WHERE job_id=?`).run(id2);
5330
+ return db.prepare(`DELETE FROM jobs WHERE id=?`).run(id2).changes > 0;
5331
+ });
5182
5332
  }
5183
5333
  };
5184
5334
  function mapFit(r) {
@@ -5222,12 +5372,12 @@ var fitRepo = {
5222
5372
  );
5223
5373
  return this.getByJob(input.job_id);
5224
5374
  }
5225
- const id3 = newId("fit_");
5375
+ const id2 = newId("fit_");
5226
5376
  db.prepare(
5227
5377
  `INSERT INTO fit_analyses (id,job_id,score,summary,strengths,gaps,matched_keywords,missing_keywords,recommendations,created_at,updated_at)
5228
5378
  VALUES (?,?,?,?,?,?,?,?,?,?,?)`
5229
5379
  ).run(
5230
- id3,
5380
+ id2,
5231
5381
  input.job_id,
5232
5382
  input.score ?? null,
5233
5383
  input.summary ?? null,
@@ -5261,8 +5411,8 @@ var applicationRepo = {
5261
5411
  mapApplication
5262
5412
  );
5263
5413
  },
5264
- get(id3) {
5265
- const r = getDb().prepare(`SELECT * FROM applications WHERE id=?`).get(id3);
5414
+ get(id2) {
5415
+ const r = getDb().prepare(`SELECT * FROM applications WHERE id=?`).get(id2);
5266
5416
  return r ? mapApplication(r) : null;
5267
5417
  },
5268
5418
  getByJob(jobId) {
@@ -5277,14 +5427,15 @@ var applicationRepo = {
5277
5427
  const existing = this.getByJob(jobId);
5278
5428
  if (existing) return existing;
5279
5429
  const ts = now();
5280
- const id3 = newId("app_");
5430
+ const id2 = newId("app_");
5281
5431
  getDb().prepare(
5282
5432
  `INSERT INTO applications (id,job_id,status,resume_id,cover_letter_id,applied_at,notes,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?)`
5283
- ).run(id3, jobId, status, null, null, null, null, ts, ts);
5284
- return this.get(id3);
5433
+ ).run(id2, jobId, status, null, null, null, null, ts, ts);
5434
+ return this.get(id2);
5285
5435
  },
5286
5436
  upsert(input) {
5287
5437
  const app = this.ensure(input.job_id, input.status ?? "draft");
5438
+ if (input.status && input.status !== app.status) assertStatusTransition(app.status, input.status);
5288
5439
  const m = { ...app, ...input };
5289
5440
  getDb().prepare(
5290
5441
  `UPDATE applications SET status=?,resume_id=?,cover_letter_id=?,applied_at=?,notes=?,updated_at=? WHERE id=?`
@@ -5293,6 +5444,7 @@ var applicationRepo = {
5293
5444
  },
5294
5445
  setStatus(jobId, status, note) {
5295
5446
  const app = this.ensure(jobId);
5447
+ assertStatusTransition(app.status, status);
5296
5448
  const appliedAt = status === "applied" && !app.applied_at ? now() : app.applied_at;
5297
5449
  const notes = note ? [app.notes, note].filter(Boolean).join("\n") : app.notes;
5298
5450
  getDb().prepare(`UPDATE applications SET status=?, applied_at=?, notes=?, updated_at=? WHERE id=?`).run(status, appliedAt, notes, now(), app.id);
@@ -5339,11 +5491,11 @@ var interviewRepo = {
5339
5491
  );
5340
5492
  return this.getByJob(input.job_id);
5341
5493
  }
5342
- const id3 = newId("itv_");
5494
+ const id2 = newId("itv_");
5343
5495
  db.prepare(
5344
5496
  `INSERT INTO interview_preps (id,job_id,questions,star_guides,self_introduction,notes,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?)`
5345
5497
  ).run(
5346
- id3,
5498
+ id2,
5347
5499
  input.job_id,
5348
5500
  toJson(input.questions ?? []),
5349
5501
  toJson(input.star_guides ?? []),
@@ -5358,216 +5510,20 @@ var interviewRepo = {
5358
5510
  var activityRepo = {
5359
5511
  log(type, summary, entity_type, entity_id) {
5360
5512
  const ts = now();
5361
- const id3 = newId("act_");
5513
+ const id2 = newId("act_");
5362
5514
  getDb().prepare(
5363
5515
  `INSERT INTO activities (id,type,entity_type,entity_id,summary,created_at) VALUES (?,?,?,?,?,?)`
5364
- ).run(id3, type, entity_type ?? null, entity_id ?? null, summary, ts);
5365
- return { id: id3, type, entity_type: entity_type ?? null, entity_id: entity_id ?? null, summary, created_at: ts };
5516
+ ).run(id2, type, entity_type ?? null, entity_id ?? null, summary, ts);
5517
+ return { id: id2, type, entity_type: entity_type ?? null, entity_id: entity_id ?? null, summary, created_at: ts };
5366
5518
  },
5367
5519
  recent(limit = 20) {
5368
5520
  return getDb().prepare(`SELECT * FROM activities ORDER BY created_at DESC LIMIT ?`).all(limit);
5369
5521
  }
5370
5522
  };
5371
5523
 
5372
- // apps/web/src/security.ts
5373
- import crypto from "node:crypto";
5374
- var SESSION_TOKEN = crypto.randomBytes(24).toString("hex");
5375
- var TOKEN_HEADER = "x-careermate-token";
5376
- var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
5377
- function hostOnly(value) {
5378
- if (!value) return null;
5379
- if (value.startsWith("[")) {
5380
- const end = value.indexOf("]");
5381
- return end === -1 ? value.toLowerCase() : value.slice(0, end + 1).toLowerCase();
5382
- }
5383
- return value.split(":")[0].toLowerCase();
5384
- }
5385
- function isLoopbackHost(hostHeader) {
5386
- const h = hostOnly(hostHeader);
5387
- return h != null && LOOPBACK_HOSTS.has(h);
5388
- }
5389
- function originHost(origin) {
5390
- if (!origin || origin === "null") return null;
5391
- try {
5392
- return new URL(origin).hostname.toLowerCase();
5393
- } catch {
5394
- return null;
5395
- }
5396
- }
5397
- var MUTATING = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
5398
- function checkRequest(req) {
5399
- if (!isLoopbackHost(req.headers.host)) {
5400
- return { ok: false, status: 403, reason: "invalid_host" };
5401
- }
5402
- const origin = req.headers.origin;
5403
- if (origin) {
5404
- const oh = originHost(origin);
5405
- if (oh == null || !LOOPBACK_HOSTS.has(oh)) {
5406
- return { ok: false, status: 403, reason: "cross_origin" };
5407
- }
5408
- }
5409
- if (MUTATING.has(req.method ?? "")) {
5410
- const token = req.headers[TOKEN_HEADER];
5411
- const provided = Array.isArray(token) ? token[0] : token;
5412
- if (!provided || !timingSafeEqual(provided, SESSION_TOKEN)) {
5413
- return { ok: false, status: 403, reason: "invalid_csrf_token" };
5414
- }
5415
- }
5416
- return { ok: true };
5417
- }
5418
- function timingSafeEqual(a, b) {
5419
- const ab = Buffer.from(a);
5420
- const bb = Buffer.from(b);
5421
- if (ab.length !== bb.length) return false;
5422
- return crypto.timingSafeEqual(ab, bb);
5423
- }
5424
- function injectToken(html) {
5425
- const tag = `<meta name="careermate-token" content="${SESSION_TOKEN}">`;
5426
- if (html.includes("</head>")) return html.replace("</head>", ` ${tag}
5427
- </head>`);
5428
- return tag + html;
5429
- }
5430
-
5431
- // apps/web/src/http.ts
5524
+ // packages/mcp-tools/src/tools.ts
5432
5525
  import fs3 from "node:fs";
5433
- import path3 from "node:path";
5434
- var MAX_BODY = 8 * 1024 * 1024;
5435
- var Router = class {
5436
- routes = [];
5437
- add(method, path6, handler) {
5438
- const keys = [];
5439
- const pattern = new RegExp(
5440
- "^" + path6.replace(/:[^/]+/g, (m) => {
5441
- keys.push(m.slice(1));
5442
- return "([^/]+)";
5443
- }) + "/?$"
5444
- );
5445
- this.routes.push({ method: method.toUpperCase(), pattern, keys, handler });
5446
- return this;
5447
- }
5448
- get(p, h) {
5449
- return this.add("GET", p, h);
5450
- }
5451
- post(p, h) {
5452
- return this.add("POST", p, h);
5453
- }
5454
- put(p, h) {
5455
- return this.add("PUT", p, h);
5456
- }
5457
- patch(p, h) {
5458
- return this.add("PATCH", p, h);
5459
- }
5460
- delete(p, h) {
5461
- return this.add("DELETE", p, h);
5462
- }
5463
- match(method, pathname) {
5464
- for (const r of this.routes) {
5465
- if (r.method !== method) continue;
5466
- const m = r.pattern.exec(pathname);
5467
- if (!m) continue;
5468
- const params = {};
5469
- r.keys.forEach((k, i) => params[k] = decodeURIComponent(m[i + 1]));
5470
- return { handler: r.handler, params };
5471
- }
5472
- return null;
5473
- }
5474
- };
5475
- function sendJson(res, status, data) {
5476
- const body = JSON.stringify(data);
5477
- res.writeHead(status, {
5478
- "Content-Type": "application/json; charset=utf-8",
5479
- "Cache-Control": "no-store",
5480
- "X-Content-Type-Options": "nosniff"
5481
- });
5482
- res.end(body);
5483
- }
5484
- function sendDownload(res, filename, mimeType, content) {
5485
- const safe = filename.replace(/[^\w.\-가-힣 ]/g, "_");
5486
- res.writeHead(200, {
5487
- "Content-Type": mimeType,
5488
- "Content-Disposition": `attachment; filename*=UTF-8''${encodeURIComponent(safe)}`,
5489
- "Cache-Control": "no-store",
5490
- "X-Content-Type-Options": "nosniff"
5491
- });
5492
- res.end(content);
5493
- }
5494
- var HttpError = class extends Error {
5495
- constructor(status, message, code) {
5496
- super(message);
5497
- this.status = status;
5498
- this.code = code;
5499
- }
5500
- };
5501
- async function readJsonBody(req, schema) {
5502
- const raw = await readBody(req);
5503
- if (!raw) return schema ? schema.parse({}) : {};
5504
- let parsed;
5505
- try {
5506
- parsed = JSON.parse(raw);
5507
- } catch {
5508
- throw new HttpError(400, "\uC798\uBABB\uB41C JSON \uD615\uC2DD\uC785\uB2C8\uB2E4.", "bad_json");
5509
- }
5510
- if (!schema) return parsed;
5511
- try {
5512
- return schema.parse(parsed);
5513
- } catch (e) {
5514
- if (e instanceof ZodError) {
5515
- const msg = e.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ");
5516
- throw new HttpError(400, `\uC785\uB825\uAC12 \uAC80\uC99D \uC2E4\uD328 \u2014 ${msg}`, "validation");
5517
- }
5518
- throw e;
5519
- }
5520
- }
5521
- function readBody(req) {
5522
- return new Promise((resolve, reject) => {
5523
- let size = 0;
5524
- const chunks = [];
5525
- req.on("data", (c) => {
5526
- size += c.length;
5527
- if (size > MAX_BODY) {
5528
- reject(new HttpError(413, "\uC694\uCCAD \uBCF8\uBB38\uC774 \uB108\uBB34 \uD07D\uB2C8\uB2E4.", "too_large"));
5529
- req.destroy();
5530
- return;
5531
- }
5532
- chunks.push(c);
5533
- });
5534
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
5535
- req.on("error", reject);
5536
- });
5537
- }
5538
- var MIME = {
5539
- ".html": "text/html; charset=utf-8",
5540
- ".js": "text/javascript; charset=utf-8",
5541
- ".mjs": "text/javascript; charset=utf-8",
5542
- ".css": "text/css; charset=utf-8",
5543
- ".json": "application/json; charset=utf-8",
5544
- ".svg": "image/svg+xml",
5545
- ".png": "image/png",
5546
- ".jpg": "image/jpeg",
5547
- ".ico": "image/x-icon",
5548
- ".woff2": "font/woff2",
5549
- ".map": "application/json"
5550
- };
5551
- function serveStatic(res, root, relPath, transform) {
5552
- const cleaned = decodeURIComponent(relPath).replace(/\0/g, "");
5553
- const resolved = path3.resolve(root, "." + path3.posix.normalize("/" + cleaned));
5554
- if (resolved !== root && !resolved.startsWith(root + path3.sep)) {
5555
- res.writeHead(403).end("Forbidden");
5556
- return true;
5557
- }
5558
- if (!fs3.existsSync(resolved) || !fs3.statSync(resolved).isFile()) return false;
5559
- const ext = path3.extname(resolved).toLowerCase();
5560
- const mime = MIME[ext] ?? "application/octet-stream";
5561
- let payload = fs3.readFileSync(resolved);
5562
- if (transform && ext === ".html") payload = transform(payload.toString("utf8"));
5563
- res.writeHead(200, {
5564
- "Content-Type": mime,
5565
- "X-Content-Type-Options": "nosniff",
5566
- "Cache-Control": ext === ".html" ? "no-store" : "no-cache"
5567
- });
5568
- res.end(payload);
5569
- return true;
5570
- }
5526
+ import path4 from "node:path";
5571
5527
 
5572
5528
  // packages/core/src/onboarding.ts
5573
5529
  function profileCompleteness(p) {
@@ -5677,8 +5633,23 @@ function addResume(input) {
5677
5633
  activityRepo.log("resume_added", `${doc.title} \uBB38\uC11C\uB97C \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "document", doc.id);
5678
5634
  return doc;
5679
5635
  }
5680
- function saveJobPosting(input, id3) {
5681
- const job = jobRepo.upsert(input, id3);
5636
+ function addExperience(input) {
5637
+ const exp = experienceRepo.add(input);
5638
+ activityRepo.log("profile_updated", `\uACBD\uB825(${exp.company})\uC744 \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "experience", exp.id);
5639
+ return exp;
5640
+ }
5641
+ function addProject(input) {
5642
+ const prj = projectRepo.add(input);
5643
+ activityRepo.log("profile_updated", `\uD504\uB85C\uC81D\uD2B8(${prj.name})\uB97C \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "project", prj.id);
5644
+ return prj;
5645
+ }
5646
+ function addSkill(input) {
5647
+ const skill = skillRepo.add(input);
5648
+ activityRepo.log("profile_updated", `\uAE30\uC220(${skill.name})\uC744 \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "skill", skill.id);
5649
+ return skill;
5650
+ }
5651
+ function saveJobPosting(input, id2) {
5652
+ const job = jobRepo.upsert(input, id2);
5682
5653
  const application = applicationRepo.ensure(job.id, "draft");
5683
5654
  activityRepo.log("job_saved", `${job.company} \xB7 ${job.position} \uACF5\uACE0\uB97C \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4.`, "job", job.id);
5684
5655
  return { job, application };
@@ -5734,141 +5705,29 @@ function updateApplicationStatus(jobId, status, note) {
5734
5705
  function saveInterviewPrep(input) {
5735
5706
  const job = jobRepo.get(input.job_id);
5736
5707
  if (!job) throw new Error(`\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.job_id}`);
5737
- applicationRepo.ensure(job.id);
5708
+ const app = applicationRepo.getByJob(job.id);
5709
+ if (!app || !INTERVIEW_UNLOCK_STATUSES.includes(app.status)) {
5710
+ const current = app ? APPLICATION_STATUS_LABELS[app.status] : "\uBBF8\uC9C0\uC6D0";
5711
+ throw new Error(
5712
+ `\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.`
5713
+ );
5714
+ }
5738
5715
  const prep = interviewRepo.save(input);
5739
5716
  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
5717
  return prep;
5741
5718
  }
5742
5719
 
5743
5720
  // packages/core/src/summary.ts
5744
- var ACTIVE = ["draft", "planned", "applied", "document_passed", "interview"];
5745
5721
  function jobWithMeta(job) {
5746
5722
  const app = applicationRepo.getByJob(job.id);
5747
5723
  const fit = fitRepo.getByJob(job.id);
5748
5724
  const status = app?.status ?? "draft";
5749
5725
  return { ...job, status, status_label: APPLICATION_STATUS_LABELS[status], fit_score: fit?.score ?? null };
5750
5726
  }
5751
- function getHomeSummary() {
5752
- const onboarding = getOnboardingStatus();
5753
- const jobs = jobRepo.list();
5754
- const applications = applicationRepo.list();
5755
- const coverLetters = coverLetterRepo.list();
5756
- const status_breakdown = Object.keys(APPLICATION_STATUS_LABELS).map((status) => ({
5757
- status,
5758
- label: APPLICATION_STATUS_LABELS[status],
5759
- count: applications.filter((a) => a.status === status).length
5760
- }));
5761
- const active = applications.filter((a) => ACTIVE.includes(a.status));
5762
- const interview_todo = applications.filter((a) => INTERVIEW_UNLOCK_STATUSES.includes(a.status) && !interviewRepo.getByJob(a.job_id)).map((a) => {
5763
- const job = jobRepo.get(a.job_id);
5764
- return job ? { job, status_label: APPLICATION_STATUS_LABELS[a.status] } : null;
5765
- }).filter((x) => x !== null);
5766
- return {
5767
- onboarding,
5768
- counts: {
5769
- jobs: jobs.length,
5770
- active_applications: active.length,
5771
- cover_letters: coverLetters.length,
5772
- interview_pending: interview_todo.length
5773
- },
5774
- status_breakdown,
5775
- recent_jobs: jobs.slice(0, 6).map(jobWithMeta),
5776
- in_progress: active.slice(0, 6).map((application) => {
5777
- const job = jobRepo.get(application.job_id);
5778
- return {
5779
- application,
5780
- job,
5781
- status_label: APPLICATION_STATUS_LABELS[application.status]
5782
- };
5783
- }),
5784
- recent_cover_letters: coverLetters.slice(0, 5),
5785
- interview_todo,
5786
- recent_activity: activityRepo.recent(10)
5787
- };
5788
- }
5789
5727
  function listRecentActivity(limit = 20) {
5790
5728
  return activityRepo.recent(limit);
5791
5729
  }
5792
5730
 
5793
- // packages/prompts/src/system.ts
5794
- var CAREERMATE_SYSTEM_PROMPT = `\uB2F9\uC2E0\uC740 \uC0AC\uC6A9\uC790\uC758 'CareerMate' \uCEE4\uB9AC\uC5B4 \uBE44\uC11C\uC785\uB2C8\uB2E4.
5795
-
5796
- CareerMate\uB294 \uC0AC\uC6A9\uC790\uC758 \uCEF4\uD4E8\uD130\uC5D0\uC11C \uB3D9\uC791\uD558\uB294 \uB85C\uCEEC \uCEE4\uB9AC\uC5B4 \uAD00\uB9AC \uB3C4\uAD6C\uC774\uBA70, MCP(\uD234 \uD638\uCD9C)\uB97C \uD1B5\uD574 \uC0AC\uC6A9\uC790\uC758 \uD504\uB85C\uD544\xB7\uC774\uB825\uC11C\xB7\uC790\uAE30\uC18C\uAC1C\uC11C\xB7\uC9C0\uC6D0 \uACF5\uACE0\xB7\uC9C0\uC6D0 \uD604\uD669 \uB370\uC774\uD130\uB97C \uC77D\uACE0 \uC501\uB2C8\uB2E4. CareerMate \uC548\uC5D0\uB294 \uBCC4\uB3C4\uC758 AI\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uBAA8\uB4E0 \uBD84\uC11D\xB7\uC791\uC131\xB7\uC694\uC57D\uC740 '\uB2F9\uC2E0'(\uC0AC\uC6A9\uC790\uC758 AI)\uC774 MCP \uD234\uC744 \uD638\uCD9C\uD574 \uC218\uD589\uD569\uB2C8\uB2E4.
5797
-
5798
- # \uD575\uC2EC \uD589\uB3D9 \uC6D0\uCE59
5799
-
5800
- 1. \uD56D\uC0C1 \uB9E5\uB77D\uC744 \uBA3C\uC800 \uBD88\uB7EC\uC635\uB2C8\uB2E4.
5801
- - \uACF5\uACE0\uB97C \uBD84\uC11D\uD558\uAC70\uB098 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC4F0\uAE30 \uC804\uC5D0\uB294 \uBC18\uB4DC\uC2DC \`get_application_context\`\uB97C \uBA3C\uC800 \uD638\uCD9C\uD558\uC138\uC694. \uC774 \uD55C \uBC88\uC758 \uD638\uCD9C\uB85C \uD504\uB85C\uD544, \uB300\uD45C \uC774\uB825\uC11C, \uACBD\uB825(experiences), \uD504\uB85C\uC81D\uD2B8(projects), \uC2A4\uD0AC(skills), \uAE30\uC874 \uC790\uAE30\uC18C\uAC1C\uC11C, \uCD5C\uADFC \uC9C0\uC6D0 \uD604\uD669, \uB300\uC0C1 \uACF5\uACE0(job_id \uC804\uB2EC \uC2DC), \uC774\uC804 \uC801\uD569\uB3C4 \uBD84\uC11D, \uAC19\uC740 \uD68C\uC0AC/\uC9C1\uBB34 \uAD00\uB828 \uAE30\uB85D, \uADF8\uB9AC\uACE0 \uC0AC\uC6A9\uC790\uC758 \uAE00\uC4F0\uAE30 \uC120\uD638(\uC120\uD638 \uBB38\uCCB4 preferred_tone + \uAC15\uC870 \uD3EC\uC778\uD2B8 emphasis_points)\uB97C \uD55C\uAEBC\uBC88\uC5D0 \uBC1B\uC2B5\uB2C8\uB2E4.
5802
- - \uC77C\uBC18\uC801\uC778 \uC0C1\uD0DC \uD655\uC778\uC774 \uD544\uC694\uD558\uBA74 \`get_onboarding_status\`, \`get_profile\`, \`get_resumes\`, \`get_cover_letters\`, \`list_recent_activity\` \uB4F1 \uC77D\uAE30 \uD234\uC744 \uC801\uADF9 \uC0AC\uC6A9\uD558\uC138\uC694.
5803
- - \uAE30\uC5B5\uC5D0 \uC758\uC874\uD558\uC9C0 \uB9D0\uACE0, \uB9E4 \uC791\uC5C5 \uC2DC\uC791 \uC2DC \uC2E4\uC81C \uB370\uC774\uD130\uB97C \uB2E4\uC2DC \uBD88\uB7EC\uC624\uC138\uC694.
5804
-
5805
- 2. \uACB0\uACFC\uB294 \uBC18\uB4DC\uC2DC \uB2E4\uC2DC \uC800\uC7A5\uD569\uB2C8\uB2E4.
5806
- - \uBD84\uC11D\xB7\uC791\uC131\uD55C \uB0B4\uC6A9\uC740 \uBA38\uB9BF\uC18D\uC774\uB098 \uB300\uD654\uCC3D\uC5D0\uB9CC \uB0A8\uAE30\uC9C0 \uB9D0\uACE0 \uD574\uB2F9 \uC800\uC7A5 \uD234\uB85C CareerMate\uC5D0 \uAE30\uB85D\uD558\uC138\uC694: \uACF5\uACE0\uB294 \`save_job_posting\`, \uC801\uD569\uB3C4 \uBD84\uC11D\uC740 \`save_fit_analysis\`, \uC790\uAE30\uC18C\uAC1C\uC11C \uBC84\uC804\uC740 \`save_cover_letter_version\`, \uBA74\uC811 \uC900\uBE44\uB294 \`save_interview_prep\`, \uC0C1\uD0DC \uBCC0\uACBD\uC740 \`update_application_status\`.
5807
- - \uC800\uC7A5 \uD6C4\uC5D0\uB294 \uBB34\uC5C7\uC774 \uC800\uC7A5\uB418\uC5C8\uB294\uC9C0 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uAC04\uB2E8\uD788 \uC54C\uB824\uC8FC\uC138\uC694.
5808
-
5809
- 3. \uB370\uC774\uD130\uB97C \uC9C0\uC5B4\uB0B4\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.
5810
- - \uD504\uB85C\uD544\xB7\uACBD\uB825\xB7\uC131\uACFC\xB7\uC218\uCE58\xB7\uD68C\uC0AC\uBA85 \uB4F1\uC740 \uC0AC\uC6A9\uC790\uAC00 \uC81C\uACF5\uD588\uAC70\uB098 CareerMate\uC5D0 \uC800\uC7A5\uB41C \uAC83\uB9CC \uC0AC\uC6A9\uD558\uC138\uC694. \uBD88\uD655\uC2E4\uD558\uBA74 \uCD94\uCE21\uD558\uC9C0 \uB9D0\uACE0 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uBB3C\uC5B4\uBCF4\uC138\uC694.
5811
- - \uC790\uAE30\uC18C\uAC1C\uC11C\uC5D0 \uAC80\uC99D\uB418\uC9C0 \uC54A\uC740 \uACBD\uD5D8\uC774\uB098 \uACFC\uC7A5\uB41C \uC131\uACFC\uB97C \uB123\uC9C0 \uB9C8\uC138\uC694. \uBE48\uCE78\uC774 \uC788\uC73C\uBA74 "\uC774 \uBD80\uBD84\uC740 \uD655\uC778\uC774 \uD544\uC694\uD569\uB2C8\uB2E4"\uB77C\uACE0 \uD45C\uC2DC\uD558\uACE0 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uD655\uC778\uC744 \uC694\uCCAD\uD558\uC138\uC694.
5812
-
5813
- 4. \uB85C\uCEEC-\uD37C\uC2A4\uD2B8 / \uD504\uB77C\uC774\uBC84\uC2DC\uB97C \uC874\uC911\uD569\uB2C8\uB2E4.
5814
- - \uBAA8\uB4E0 \uB370\uC774\uD130\uB294 \uC0AC\uC6A9\uC790\uC758 \uCEF4\uD4E8\uD130\uC5D0\uB9CC \uC800\uC7A5\uB429\uB2C8\uB2E4. \uB370\uC774\uD130\uB97C \uC678\uBD80\uB85C \uBCF4\uB0B4\uAC70\uB098 \uB2E4\uB978 \uC6A9\uB3C4\uB85C \uC4F0\uC9C0 \uB9C8\uC138\uC694.
5815
- - \uBBFC\uAC10 \uC815\uBCF4(\uC5F0\uB77D\uCC98, \uC8FC\uC18C \uB4F1)\uB294 \uAF2D \uD544\uC694\uD55C \uC791\uC5C5\uC5D0\uB9CC \uC0AC\uC6A9\uD558\uACE0 \uBD88\uD544\uC694\uD558\uAC8C \uB178\uCD9C\uD558\uC9C0 \uB9C8\uC138\uC694.
5816
-
5817
- 5. \uC790\uAE30\uC18C\uAC1C\uC11C\uB294 \uC791\uC131 \uC804 \uD655\uC778\uC744 \uBC1B\uC2B5\uB2C8\uB2E4.
5818
- - \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC0C8\uB85C \uC4F0\uAC70\uB098 \uD06C\uAC8C \uACE0\uCE58\uAE30 \uC804\uC5D0, \uC5B4\uB5A4 \uACF5\uACE0\uB97C \uB300\uC0C1\uC73C\uB85C \uC5B4\uB5A4 \uD1A4\uACFC \uAC15\uC870\uC810\uC73C\uB85C \uC4F8\uC9C0 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uBA3C\uC800 \uD655\uC778\uD558\uC138\uC694.
5819
- - \uC791\uC131 \uD6C4\uC5D0\uB294 \`save_cover_letter_version\`\uC73C\uB85C \uBC84\uC804\uC744 \uC800\uC7A5\uD558\uACE0, \uBCC0\uACBD \uC694\uC57D\uC744 \`note\`\uC5D0 \uB0A8\uAE30\uC138\uC694. \uC0AC\uC6A9\uC790\uAC00 \uC6D0\uD558\uBA74 \`export_cover_letter\`\uB85C \uB0B4\uBCF4\uB0B4\uC138\uC694.
5820
-
5821
- 6. \uC0AC\uC6A9\uC790\uC5D0\uAC8C\uB294 \uC26C\uC6B4 \uD55C\uAD6D\uC5B4\uB85C \uC54C\uB9BD\uB2C8\uB2E4.
5822
- - "MCP", "\uD234 \uD638\uCD9C", "job_id" \uAC19\uC740 \uAE30\uC220 \uC6A9\uC5B4\uB294 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uC9C1\uC811 \uB178\uCD9C\uD558\uC9C0 \uB9C8\uC138\uC694. "\uACF5\uACE0\uB97C \uC800\uC7A5\uD588\uC5B4\uC694", "\uC801\uD569\uB3C4\uB97C \uBD84\uC11D\uD588\uC5B4\uC694"\uCC98\uB7FC \uC77C\uC0C1\uC801\uC778 \uD45C\uD604\uC73C\uB85C \uC9C4\uD589 \uC0C1\uD669\uC744 \uC54C\uB9AC\uC138\uC694.
5823
- - \uBB34\uC5C7\uC744 \uD588\uACE0, \uBB34\uC5C7\uC744 \uC54C\uAC8C \uB418\uC5C8\uACE0, \uB2E4\uC74C\uC5D0 \uBB34\uC5C7\uC744 \uD560 \uC218 \uC788\uB294\uC9C0 \uC9E7\uACE0 \uBA85\uD655\uD558\uAC8C \uC804\uB2EC\uD558\uC138\uC694.
5824
-
5825
- 7. \uB2E4\uC74C \uB2E8\uACC4\uB97C \uBA3C\uC800 \uC81C\uC548\uD569\uB2C8\uB2E4.
5826
- - \uC791\uC5C5\uC774 \uB05D\uB098\uBA74 \uC790\uC5F0\uC2A4\uB7EC\uC6B4 \uB2E4\uC74C \uD589\uB3D9\uC744 \uC81C\uC548\uD558\uC138\uC694. \uC608: \uACF5\uACE0 \uBD84\uC11D \uD6C4 \u2192 "\uC774 \uACF5\uACE0\uC5D0 \uB9DE\uCD98 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC368\uB4DC\uB9B4\uAE4C\uC694?", \uC790\uAE30\uC18C\uAC1C\uC11C \uC800\uC7A5 \uD6C4 \u2192 "\uC9C0\uC6D0 \uC0C1\uD0DC\uB97C '\uC9C0\uC6D0 \uC644\uB8CC'\uB85C \uBC14\uAFC0\uAE4C\uC694?", \uC0C1\uD0DC\uAC00 '\uC11C\uB958 \uD569\uACA9'\uC774 \uB418\uBA74 \u2192 "\uBA74\uC811 \uC900\uBE44\uB97C \uB3C4\uC640\uB4DC\uB9B4\uAE4C\uC694?".
5827
- - \`get_onboarding_status\`\uC758 next_steps\uB098 \`open_dashboard\`/\`open_application\`\uC744 \uD65C\uC6A9\uD574 \uC0AC\uC6A9\uC790\uB97C \uB2E4\uC74C \uD589\uB3D9\uC73C\uB85C \uC548\uB0B4\uD558\uC138\uC694.
5828
-
5829
- 8. \uAE00\uC740 AI \uD2F0 \uC548 \uB098\uAC8C \uC501\uB2C8\uB2E4.
5830
- - \uC790\uAE30\uC18C\uAC1C\uC11C\xB7\uC790\uAE30 PR\xB7\uC9C0\uC6D0 \uBA54\uC77C \uB4F1 \uC0AC\uB78C\uC774 \uC4F4 \uB4EF\uD55C \uAE00\uC744 \uC791\uC131\uD558\uAE30 \uC9C1\uC804\uC5D0\uB294 \`get_writing_style_guide\`\uB97C \uD638\uCD9C\uD574 \uAE00\uC4F0\uAE30 \uADDC\uCE59\uC744 \uAC00\uC838\uC624\uACE0 \uADF8\uB300\uB85C \uC801\uC6A9\uD558\uC138\uC694.
5831
- - \uBC88\uC5ED\uD22C, "\uC5F4\uC815\uC744 \uAC00\uC9C0\uACE0/\uADC0\uC0AC\uC758 \uBC1C\uC804\uC5D0 \uAE30\uC5EC" \uAC19\uC740 \uD074\uB9AC\uC170, \uAE30\uACC4\uC801 \uBCD1\uB82C(\uCCAB\uC9F8\xB7\uB458\uC9F8\xB7\uC14B\uC9F8), \uC0C1\uD22C\uC801 \uC5F0\uACB0\uC5B4, \uADE0\uC77C\uD55C \uBB38\uC7A5 \uB9AC\uB4EC\uC744 \uD53C\uD558\uACE0 \uB2F4\uBC31\uD558\uACE0 \uAD6C\uCCB4\uC801\uC73C\uB85C \uC501\uB2C8\uB2E4. \uB2E8, \uC0AC\uC2E4\xB7\uC218\uCE58\xB7\uACE0\uC720\uBA85\uC0AC\uB294 \uC808\uB300 \uBC14\uAFB8\uAC70\uB098 \uC9C0\uC5B4\uB0B4\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.
5832
-
5833
- # \uC791\uC5C5 \uBC29\uC2DD \uC694\uC57D
5834
- - \uC77D\uAE30(get_*) \u2192 \uBD84\uC11D/\uC791\uC131 \u2192 \uC800\uC7A5(save_*/update_*) \u2192 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uC26C\uC6B4 \uB9D0\uB85C \uBCF4\uACE0 \u2192 \uB2E4\uC74C \uB2E8\uACC4 \uC81C\uC548.
5835
- - 8\uAC00\uC9C0 \uC9C0\uC6D0 \uC0C1\uD0DC: \uC791\uC131 \uC911(draft), \uC9C0\uC6D0 \uC608\uC815(planned), \uC9C0\uC6D0 \uC644\uB8CC(applied), \uC11C\uB958 \uD569\uACA9(document_passed), \uBA74\uC811 \uC9C4\uD589(interview), \uCD5C\uC885 \uD569\uACA9(final_passed), \uBD88\uD569\uACA9(rejected), \uBCF4\uB958(on_hold).
5836
- - \uC758\uC2EC\uC2A4\uB7EC\uC6B0\uBA74 \uBA48\uCD94\uACE0 \uBB3B\uC2B5\uB2C8\uB2E4. \uC0AC\uC6A9\uC790\uC758 \uCEE4\uB9AC\uC5B4\uAC00 \uAC78\uB9B0 \uC77C\uC774\uBBC0\uB85C \uC815\uD655\uD568\uC774 \uC18D\uB3C4\uBCF4\uB2E4 \uC911\uC694\uD569\uB2C8\uB2E4.`;
5837
-
5838
- // packages/prompts/src/install.ts
5839
- var INSTALL_PROMPT_CLAUDE = `Claude\uC57C, \uC544\uB798 CareerMate \uC124\uCE58 \uD398\uC774\uC9C0(\uB610\uB294 INSTALL.md)\uB97C \uC77D\uACE0 \uB0B4 \uCEF4\uD4E8\uD130\uC5D0 CareerMate\uB97C \uC124\uCE58\uD558\uACE0 \uC124\uC815\uD574\uC918.
5840
-
5841
- \uC21C\uC11C\uB294 \uC774\uB807\uAC8C \uC9C4\uD589\uD574\uC918.
5842
- 1. \uC124\uCE58 \uC548\uB0B4 \uD398\uC774\uC9C0 / INSTALL.md \uB0B4\uC6A9\uC744 \uB05D\uAE4C\uC9C0 \uC77D\uACE0 \uADF8\uB300\uB85C \uB530\uB77C \uD574.
5843
- 2. CareerMate MCP \uC11C\uBC84\uB97C \uB0B4 Claude\uC5D0 \uC5F0\uACB0(\uC124\uC815)\uD574\uC918.
5844
- 3. \uC5F0\uACB0\uC774 \uB05D\uB098\uBA74 \`get_onboarding_status\`\uB97C \uD638\uCD9C\uD574\uC11C \uC815\uB9D0\uB85C \uC5F0\uACB0\uB418\uC5C8\uB294\uC9C0 \uD655\uC778\uD558\uACE0, \uACB0\uACFC\uB97C \uC26C\uC6B4 \uB9D0\uB85C \uC54C\uB824\uC918.
5845
- 4. \uADF8\uB2E4\uC74C \uB098\uC5D0\uAC8C \uC774\uB825\uC11C\uB098 \uC790\uAE30\uC18C\uAC1C\uC11C \uD30C\uC77C\uC744 \uC5C5\uB85C\uB4DC\uD558\uB77C\uACE0 \uC548\uB0B4\uD574\uC918.
5846
- 5. \uB0B4\uAC00 \uC62C\uB9B0 \uD30C\uC77C\uC744 \uBC14\uD0D5\uC73C\uB85C \uB0B4 \uD504\uB85C\uD544\uC744 \uAD6C\uC870\uD654\uD574\uC11C \`save_profile\`\uB85C CareerMate\uC5D0 \uC800\uC7A5\uD558\uACE0, \uC628\uBCF4\uB529\uC744 \uC2DC\uC791\uD574\uC918.
5847
-
5848
- \uAE30\uC220 \uC6A9\uC5B4\uB294 \uCD5C\uB300\uD55C \uBE7C\uACE0, \uB9E4 \uB2E8\uACC4\uB9C8\uB2E4 \uC9C0\uAE08 \uBB58 \uD558\uACE0 \uC788\uB294\uC9C0 \uC26C\uC6B4 \uD55C\uAD6D\uC5B4\uB85C \uC54C\uB824\uC918. \uB9C9\uD788\uB294 \uBD80\uBD84\uC774 \uC788\uC73C\uBA74 \uCD94\uCE21\uD558\uC9C0 \uB9D0\uACE0 \uB098\uC5D0\uAC8C \uBB3C\uC5B4\uBD10\uC918.`;
5849
- var INSTALL_PROMPT_CHATGPT = `ChatGPT\uC57C, \uC544\uB798 \uD398\uC774\uC9C0\uB97C \uC77D\uACE0 \uB0B4 \uCEF4\uD4E8\uD130\uC5D0 CareerMate\uB97C \uC124\uCE58\uD558\uACE0 \uC124\uC815\uD574\uC918. \uC124\uCE58\uAC00 \uB05D\uB098\uBA74 \uC5F0\uACB0 \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uACE0, \uB098\uC5D0\uAC8C \uC774\uB825\uC11C\uB098 \uC790\uAE30\uC18C\uAC1C\uC11C \uD30C\uC77C\uC744 \uC5C5\uB85C\uB4DC\uD558\uB77C\uACE0 \uC548\uB0B4\uD574\uC918. \uADF8\uB2E4\uC74C \uB0B4 \uD504\uB85C\uD544\uC744 \uAD6C\uC870\uD654\uD574\uC11C CareerMate\uC5D0 \uC800\uC7A5\uD574\uC918.
5850
-
5851
- \uAD6C\uCCB4\uC801\uC73C\uB85C\uB294 \uC774\uB807\uAC8C \uD574\uC918.
5852
- 1. CareerMate \uC124\uCE58 \uC548\uB0B4 \uD398\uC774\uC9C0 / INSTALL.md\uB97C \uC77D\uACE0 \uADF8\uB300\uB85C \uB530\uB77C \uC124\uCE58\uD574.
5853
- 2. CareerMate MCP \uCEE4\uB125\uD130\uB97C \uC5F0\uACB0(\uC124\uC815)\uD574\uC918.
5854
- 3. \uC5F0\uACB0 \uD6C4 \`get_onboarding_status\`\uB97C \uD638\uCD9C\uD574 \uC5F0\uACB0\uC774 \uC815\uC0C1\uC778\uC9C0 \uD655\uC778\uD558\uACE0 \uACB0\uACFC\uB97C \uC26C\uC6B4 \uB9D0\uB85C \uC54C\uB824\uC918.
5855
- 4. \uC774\uB825\uC11C\xB7\uC790\uAE30\uC18C\uAC1C\uC11C \uD30C\uC77C\uC744 \uC5C5\uB85C\uB4DC\uD558\uB77C\uACE0 \uB098\uC5D0\uAC8C \uC548\uB0B4\uD574\uC918.
5856
- 5. \uC62C\uB9B0 \uD30C\uC77C\uB85C \uB0B4 \uD504\uB85C\uD544\uC744 \uC815\uB9AC\uD574\uC11C \`save_profile\`\uB85C \uC800\uC7A5\uD558\uACE0 \uC628\uBCF4\uB529\uC744 \uC2DC\uC791\uD574\uC918.
5857
-
5858
- \uC9C4\uD589 \uC0C1\uD669\uC740 \uD56D\uC0C1 \uC26C\uC6B4 \uD55C\uAD6D\uC5B4\uB85C \uC124\uBA85\uD558\uACE0, \uD655\uC2E4\uD558\uC9C0 \uC54A\uC740 \uC815\uBCF4\uB294 \uC9C0\uC5B4\uB0B4\uC9C0 \uB9D0\uACE0 \uB098\uC5D0\uAC8C \uBB3C\uC5B4\uBD10\uC918.`;
5859
- var INSTALL_PROMPT_GENERIC = `\uC548\uB155, \uC544\uB798 CareerMate \uC124\uCE58 \uC548\uB0B4(\uC124\uCE58 \uD398\uC774\uC9C0 \uB610\uB294 INSTALL.md)\uB97C \uC77D\uACE0 \uB0B4 \uCEF4\uD4E8\uD130\uC5D0 CareerMate\uB97C \uC124\uCE58\uD558\uACE0 \uC124\uC815\uD574\uC918.
5860
-
5861
- CareerMate\uB294 \uB0B4 \uCEE4\uB9AC\uC5B4 \uB370\uC774\uD130(\uD504\uB85C\uD544\xB7\uC774\uB825\uC11C\xB7\uC790\uAE30\uC18C\uAC1C\uC11C\xB7\uC9C0\uC6D0 \uD604\uD669)\uB97C \uB0B4 \uCEF4\uD4E8\uD130\uC5D0 \uC800\uC7A5\uD558\uACE0, MCP\uB97C \uD1B5\uD574 \uB108(\uB0B4 AI)\uAC00 \uADF8 \uB370\uC774\uD130\uB97C \uC77D\uACE0 \uC4F8 \uC218 \uC788\uAC8C \uD574\uC8FC\uB294 \uB3C4\uAD6C\uC57C. \uBD84\uC11D\uACFC \uC791\uC131\uC740 \uC804\uBD80 \uB124\uAC00 \uD574.
5862
-
5863
- \uC774\uB807\uAC8C \uC9C4\uD589\uD574\uC918.
5864
- 1. \uC124\uCE58 \uC548\uB0B4 \uBB38\uC11C\uB97C \uB05D\uAE4C\uC9C0 \uC77D\uACE0 \uADF8\uB300\uB85C \uB530\uB77C \uC124\uCE58\uD574.
5865
- 2. \uB124\uAC00 \uC4F0\uB294 AI \uB3C4\uAD6C\uC5D0 CareerMate MCP \uC11C\uBC84\uB97C \uC5F0\uACB0(\uC124\uC815)\uD574\uC918.
5866
- 3. \uC5F0\uACB0 \uD6C4 \`get_onboarding_status\`\uB97C \uD638\uCD9C\uD574 \uC5F0\uACB0 \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uACE0, \uACB0\uACFC\uB97C \uC26C\uC6B4 \uD55C\uAD6D\uC5B4\uB85C \uC54C\uB824\uC918.
5867
- 4. \uB098\uC5D0\uAC8C \uC774\uB825\uC11C\uB098 \uC790\uAE30\uC18C\uAC1C\uC11C \uD30C\uC77C\uC744 \uC5C5\uB85C\uB4DC\uD558\uB77C\uACE0 \uC548\uB0B4\uD574\uC918.
5868
- 5. \uC62C\uB9B0 \uD30C\uC77C\uB85C \uB0B4 \uD504\uB85C\uD544\uC744 \uAD6C\uC870\uD654\uD574\uC11C \`save_profile\`\uB85C \uC800\uC7A5\uD558\uACE0 \uC628\uBCF4\uB529\uC744 \uC2DC\uC791\uD574\uC918.
5869
-
5870
- \uB9E4 \uB2E8\uACC4\uB9C8\uB2E4 \uC26C\uC6B4 \uD55C\uAD6D\uC5B4\uB85C \uC124\uBA85\uD558\uACE0, \uBD88\uD655\uC2E4\uD55C \uB0B4\uC6A9\uC740 \uC9C0\uC5B4\uB0B4\uC9C0 \uB9D0\uACE0 \uB098\uC5D0\uAC8C \uD655\uC778\uD574\uC918.`;
5871
-
5872
5731
  // packages/prompts/src/onboarding.ts
5873
5732
  var ONBOARDING_PROMPT = `\uC774\uC81C CareerMate\uC5D0 \uC5F0\uACB0\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC0AC\uC6A9\uC790\uC758 \uCEE4\uB9AC\uC5B4 \uB370\uC774\uD130 \uCCAB \uC14B\uC5C5(\uC628\uBCF4\uB529)\uC744 \uB3C4\uC640\uC8FC\uC138\uC694.
5874
5733
 
@@ -5946,46 +5805,6 @@ var HUMANIZE_WRITING_GUIDE = `# \uC0AC\uB78C\uC774 \uC4F4 \uAC83\uCC98\uB7FC: \u
5946
5805
  - \uC0AC\uC2E4\xB7\uC218\uCE58\xB7\uACE0\uC720\uBA85\uC0AC\uAC00 \uC6D0\uBCF8\uACFC 100% \uC77C\uCE58\uD558\uB294\uAC00?
5947
5806
  - \uC0AC\uC6A9\uC790\uC758 \uD1A4\uACFC \uAC15\uC870 \uD3EC\uC778\uD2B8\uAC00 \uC0B4\uC544 \uC788\uB294\uAC00?`;
5948
5807
 
5949
- // packages/prompts/src/index.ts
5950
- var PROMPTS = [
5951
- {
5952
- id: "system",
5953
- title: "CareerMate \uC2DC\uC2A4\uD15C \uD504\uB86C\uD504\uD2B8 (AI \uB3D9\uC791 \uBC29\uC2DD)",
5954
- audience: "system",
5955
- body: CAREERMATE_SYSTEM_PROMPT
5956
- },
5957
- {
5958
- id: "install_claude",
5959
- title: "Claude \uC124\uCE58 \uD504\uB86C\uD504\uD2B8",
5960
- audience: "claude",
5961
- body: INSTALL_PROMPT_CLAUDE
5962
- },
5963
- {
5964
- id: "install_chatgpt",
5965
- title: "ChatGPT \uC124\uCE58 \uD504\uB86C\uD504\uD2B8",
5966
- audience: "chatgpt",
5967
- body: INSTALL_PROMPT_CHATGPT
5968
- },
5969
- {
5970
- id: "install_generic",
5971
- title: "\uBC94\uC6A9 AI \uC124\uCE58 \uD504\uB86C\uD504\uD2B8",
5972
- audience: "generic",
5973
- body: INSTALL_PROMPT_GENERIC
5974
- },
5975
- {
5976
- id: "onboarding",
5977
- title: "\uC628\uBCF4\uB529 \uD504\uB86C\uD504\uD2B8 (\uC5F0\uACB0 \uC9C1\uD6C4)",
5978
- audience: "all",
5979
- body: ONBOARDING_PROMPT
5980
- },
5981
- {
5982
- id: "humanize_writing",
5983
- title: "AI \uD2F0 \uC548 \uB098\uB294 \uAE00\uC4F0\uAE30 \uAC00\uC774\uB4DC (\uC790\uAE30\uC18C\uAC1C\uC11C \uC791\uC131)",
5984
- audience: "all",
5985
- body: HUMANIZE_WRITING_GUIDE
5986
- }
5987
- ];
5988
-
5989
5808
  // packages/workflows/src/definitions.ts
5990
5809
  var WORKFLOWS = [
5991
5810
  {
@@ -6064,307 +5883,34 @@ var WORKFLOWS = [
6064
5883
  }
6065
5884
  ];
6066
5885
 
6067
- // packages/parsers/src/html.ts
6068
- var NAMED_ENTITIES = {
6069
- amp: "&",
6070
- lt: "<",
6071
- gt: ">",
6072
- quot: '"',
6073
- apos: "'",
6074
- nbsp: " ",
6075
- middot: "\xB7",
6076
- hellip: "\u2026",
6077
- mdash: "\u2014",
6078
- ndash: "\u2013",
6079
- bull: "\u2022",
6080
- copy: "\xA9",
6081
- reg: "\xAE",
6082
- trade: "\u2122"
6083
- };
6084
- function decodeEntities(input) {
6085
- return input.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (match, body) => {
6086
- if (body[0] === "#") {
6087
- const isHex = body[1] === "x" || body[1] === "X";
6088
- const code = parseInt(body.slice(isHex ? 2 : 1), isHex ? 16 : 10);
6089
- if (Number.isFinite(code) && code > 0 && code <= 1114111) {
6090
- try {
6091
- return String.fromCodePoint(code);
6092
- } catch {
6093
- return match;
6094
- }
6095
- }
6096
- return match;
6097
- }
6098
- const named = NAMED_ENTITIES[body.toLowerCase()];
6099
- return named !== void 0 ? named : match;
5886
+ // packages/workflows/src/index.ts
5887
+ function getWorkflow(id2) {
5888
+ return WORKFLOWS.find((w) => w.id === id2);
5889
+ }
5890
+ function renderWorkflowMarkdown(id2) {
5891
+ const wf = getWorkflow(id2);
5892
+ if (!wf) return void 0;
5893
+ const lines = [];
5894
+ lines.push(`# ${wf.title}`);
5895
+ lines.push("");
5896
+ lines.push(wf.description);
5897
+ lines.push("");
5898
+ lines.push(`**\uD2B8\uB9AC\uAC70:** ${wf.trigger}`);
5899
+ lines.push("");
5900
+ lines.push("## \uB2E8\uACC4");
5901
+ wf.steps.forEach((step, i) => {
5902
+ lines.push(`${i + 1}. ${step}`);
6100
5903
  });
6101
- }
6102
- var BLOCK_TAG = /<\/?(p|div|section|article|header|footer|li|ul|ol|tr|table|h[1-6]|blockquote|pre)\b[^>]*>/gi;
6103
- function stripHtml(html) {
6104
- if (!html) return "";
6105
- let s = String(html);
6106
- s = s.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, " ");
6107
- s = s.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, " ");
6108
- s = s.replace(/<!--[\s\S]*?-->/g, " ");
6109
- s = s.replace(/<br\b[^>]*>/gi, "\n");
6110
- s = s.replace(/<hr\b[^>]*>/gi, "\n");
6111
- s = s.replace(BLOCK_TAG, "\n");
6112
- s = s.replace(/<\/?[^>]+>/g, " ");
6113
- s = decodeEntities(s);
6114
- s = s.replace(/[ \t\f\v]+/g, " ");
6115
- s = s.split("\n").map((line) => line.trim()).join("\n");
6116
- s = s.replace(/\n{3,}/g, "\n\n");
6117
- return s.trim();
6118
- }
6119
-
6120
- // packages/parsers/src/keywords.ts
6121
- var TECH_TERMS = [
6122
- // Languages
6123
- { label: "JavaScript", aliases: ["js"] },
6124
- { label: "TypeScript", aliases: ["ts"] },
6125
- { label: "Python" },
6126
- { label: "Java" },
6127
- { label: "Kotlin" },
6128
- { label: "Go", aliases: ["golang"] },
6129
- { label: "Rust" },
6130
- { label: "C++", aliases: ["cpp"] },
6131
- { label: "C#", aliases: ["csharp"] },
6132
- { label: "C" },
6133
- { label: "Ruby" },
6134
- { label: "PHP" },
6135
- { label: "Swift" },
6136
- { label: "Scala" },
6137
- { label: "Dart" },
6138
- { label: "SQL" },
6139
- { label: "GraphQL" },
6140
- // Frontend frameworks/libs
6141
- { label: "React", aliases: ["react.js", "reactjs"] },
6142
- { label: "Next.js", aliases: ["nextjs", "next js"] },
6143
- { label: "Vue", aliases: ["vue.js", "vuejs"] },
6144
- { label: "Nuxt", aliases: ["nuxt.js", "nuxtjs"] },
6145
- { label: "Angular" },
6146
- { label: "Svelte" },
6147
- { label: "Redux" },
6148
- { label: "Tailwind", aliases: ["tailwindcss", "tailwind css"] },
6149
- // Backend frameworks
6150
- { label: "Node.js", aliases: ["nodejs", "node js", "node"] },
6151
- { label: "Express", aliases: ["express.js", "expressjs"] },
6152
- { label: "NestJS", aliases: ["nest.js", "nest js"] },
6153
- { label: "Spring", aliases: ["spring boot", "springboot"] },
6154
- { label: "Django" },
6155
- { label: "Flask" },
6156
- { label: "FastAPI" },
6157
- { label: "Rails", aliases: ["ruby on rails"] },
6158
- { label: "Laravel" },
6159
- { label: ".NET", aliases: ["dotnet", "asp.net"] },
6160
- // Datastores
6161
- { label: "PostgreSQL", aliases: ["postgres", "postgresql"] },
6162
- { label: "MySQL" },
6163
- { label: "MariaDB" },
6164
- { label: "MongoDB", aliases: ["mongo"] },
6165
- { label: "Redis" },
6166
- { label: "Elasticsearch", aliases: ["elastic search"] },
6167
- { label: "Oracle" },
6168
- { label: "SQLite" },
6169
- { label: "Kafka" },
6170
- { label: "RabbitMQ" },
6171
- // Cloud / infra / devops
6172
- { label: "AWS", aliases: ["amazon web services"] },
6173
- { label: "GCP", aliases: ["google cloud"] },
6174
- { label: "Azure" },
6175
- { label: "Docker" },
6176
- { label: "Kubernetes", aliases: ["k8s"] },
6177
- { label: "Terraform" },
6178
- { label: "Jenkins" },
6179
- { label: "GitHub Actions", aliases: ["github action"] },
6180
- { label: "CI/CD", aliases: ["cicd"] },
6181
- { label: "Nginx" },
6182
- { label: "Linux" },
6183
- // Tooling / misc
6184
- { label: "Git" },
6185
- { label: "REST", aliases: ["rest api", "restful"] },
6186
- { label: "gRPC" },
6187
- { label: "Webpack" },
6188
- { label: "Vite" },
6189
- { label: "Jest" },
6190
- { label: "Figma" },
6191
- { label: "Jira" },
6192
- // Data / ML
6193
- { label: "TensorFlow" },
6194
- { label: "PyTorch" },
6195
- { label: "Pandas" },
6196
- { label: "NumPy", aliases: ["numpy"] },
6197
- { label: "Spark", aliases: ["apache spark"] },
6198
- { label: "Airflow" }
6199
- ];
6200
- function escapeRegExp(s) {
6201
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6202
- }
6203
- function buildPattern(term) {
6204
- const esc = escapeRegExp(term);
6205
- if (/^[\w.]+$/.test(term) && /\w$/.test(term) && /^\w/.test(term)) {
6206
- return new RegExp(`(?<![\\w])${esc}(?![\\w])`, "i");
6207
- }
6208
- return new RegExp(`(?<![A-Za-z0-9])${esc}(?![A-Za-z0-9])`, "i");
6209
- }
6210
- function extractTechKeywords(text, limit = 20) {
6211
- if (!text) return [];
6212
- const found = [];
6213
- const seen = /* @__PURE__ */ new Set();
6214
- for (const term of TECH_TERMS) {
6215
- if (seen.has(term.label)) continue;
6216
- const candidates = [term.label, ...term.aliases ?? []];
6217
- const matched = candidates.some((c) => {
6218
- try {
6219
- return buildPattern(c).test(text);
6220
- } catch {
6221
- return false;
6222
- }
6223
- });
6224
- if (matched) {
6225
- seen.add(term.label);
6226
- found.push(term.label);
6227
- if (found.length >= limit) break;
6228
- }
6229
- }
6230
- return found;
6231
- }
6232
-
6233
- // packages/parsers/src/index.ts
6234
- var HTML_MIME_RE = /html/i;
6235
- var MD_EXT_RE = /\.(md|markdown|mdown|mkd)$/i;
6236
- var HTML_EXT_RE = /\.(html?|xhtml)$/i;
6237
- var BINARY_EXT_RE = /\.(pdf|docx?|hwpx?|pptx?|xlsx?|rtf|odt|pages|key)$/i;
6238
- var BINARY_MIME_RE = /(pdf|msword|officedocument|hancom|rtf|opendocument)/i;
6239
- function looksBinary(s) {
6240
- if (!s) return false;
6241
- const sample = s.slice(0, 2e3);
6242
- let control = 0;
6243
- for (let i = 0; i < sample.length; i++) {
6244
- const c = sample.charCodeAt(i);
6245
- if (c === 0) return true;
6246
- if (c < 9 || c > 13 && c < 32) control++;
6247
- }
6248
- return control / sample.length > 0.1;
6249
- }
6250
- function looksLikeHtml(s) {
6251
- return /<\/?[a-z][\s\S]*?>/i.test(s) && /<\/?(html|body|div|p|span|br|table|h[1-6]|ul|li)\b/i.test(s);
6252
- }
6253
- function extractText(input) {
6254
- const warnings = [];
6255
- const safe = input ?? { content: "" };
6256
- const content = typeof safe.content === "string" ? safe.content : "";
6257
- const filename = safe.filename ?? "";
6258
- const mimeType = safe.mimeType ?? "";
6259
- const isBinaryType = BINARY_EXT_RE.test(filename) || BINARY_MIME_RE.test(mimeType);
6260
- if (isBinaryType) {
6261
- if (content && !looksBinary(content)) {
6262
- warnings.push(
6263
- "\uBC14\uC774\uB108\uB9AC \uBB38\uC11C(PDF/DOCX \uB4F1)\uB294 \uC9C1\uC811 \uD30C\uC2F1\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uC774\uBBF8 \uD14D\uC2A4\uD2B8\uB85C \uBCF4\uC774\uB294 \uB0B4\uC6A9\uC744 \uADF8\uB300\uB85C \uC0AC\uC6A9\uD588\uC2B5\uB2C8\uB2E4."
6264
- );
6265
- return { text: content.trim(), format: "unsupported", warnings };
6266
- }
6267
- warnings.push(
6268
- "PDF/DOCX \uB4F1 \uBC14\uC774\uB108\uB9AC \uBB38\uC11C\uB294 \uD604\uC7AC \uC790\uB3D9 \uCD94\uCD9C\uC744 \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uB0B4\uC6A9\uC744 \uD14D\uC2A4\uD2B8\uB85C \uBCF5\uC0AC\uD574 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694."
6269
- );
6270
- return { text: "", format: "unsupported", warnings };
6271
- }
6272
- if (looksBinary(content)) {
6273
- warnings.push(
6274
- "\uC77D\uC744 \uC218 \uC5C6\uB294 \uBC14\uC774\uB108\uB9AC \uB370\uC774\uD130\uB85C \uBCF4\uC785\uB2C8\uB2E4. \uB0B4\uC6A9\uC744 \uD14D\uC2A4\uD2B8\uB85C \uBCF5\uC0AC\uD574 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694."
6275
- );
6276
- return { text: "", format: "unsupported", warnings };
6277
- }
6278
- if (HTML_MIME_RE.test(mimeType) || HTML_EXT_RE.test(filename) || looksLikeHtml(content)) {
6279
- return { text: stripHtml(content), format: "html", warnings };
6280
- }
6281
- if (MD_EXT_RE.test(filename) || /markdown/i.test(mimeType)) {
6282
- return { text: content.trim(), format: "markdown", warnings };
6283
- }
6284
- return { text: content.trim(), format: "text", warnings };
6285
- }
6286
- var BOILERPLATE_RE = /^(home|menu|login|로그인|회원가입|sign\s?up|sign\s?in|share|공유|스크랩|북마크|이전|다음|목록|prev(ious)?|next|cookie|쿠키|copyright|©|all rights reserved|이용약관|개인정보처리방침|상단으로|top|search|검색)$/i;
6287
- var COMPANY_LABEL_RE = /(?:회사명?|기업명?|company)\s*[::]\s*(.+)/i;
6288
- var POSITION_LABEL_RE = /(?:포지션|직무|모집\s*부문|채용\s*부문|position|job\s*title)\s*[::]\s*(.+)/i;
6289
- var DEADLINE_LABEL_RE = /(?:마감(?:일|일자|기한)?|접수\s*마감|deadline|due)\s*[::]?\s*(.+)/i;
6290
- function findDeadline(text) {
6291
- const rolling = /(상시\s*채용|채용\s*시\s*(?:마감|까지)|수시\s*채용)/i.exec(text);
6292
- for (const line of text.split("\n")) {
6293
- const m = DEADLINE_LABEL_RE.exec(line);
6294
- if (m) {
6295
- const date = normalizeDate(m[1]);
6296
- if (date) return date;
6297
- const tail = m[1].trim();
6298
- if (tail && tail.length <= 30) return tail;
6299
- }
6300
- }
6301
- const any = normalizeDate(text);
6302
- if (any) return any;
6303
- if (rolling) return rolling[1].replace(/\s+/g, " ").trim();
6304
- return void 0;
6305
- }
6306
- function normalizeDate(s) {
6307
- if (!s) return void 0;
6308
- let m = /(\d{4})[.\-/년]\s*(\d{1,2})[.\-/월]\s*(\d{1,2})/.exec(s);
6309
- if (m) {
6310
- const [, y, mo, d] = m;
6311
- return `${y}-${mo.padStart(2, "0")}-${d.padStart(2, "0")}`;
6312
- }
6313
- m = /(\d{4})[.\-/년]\s*(\d{1,2})\b/.exec(s);
6314
- if (m) {
6315
- const [, y, mo] = m;
6316
- return `${y}-${mo.padStart(2, "0")}`;
6317
- }
6318
- return void 0;
6319
- }
6320
- function findLabelled(lines, re) {
6321
- for (const line of lines) {
6322
- const m = re.exec(line);
6323
- if (m) {
6324
- const val = m[1].trim();
6325
- if (val && val.length <= 80) return val;
6326
- }
6327
- }
6328
- return void 0;
6329
- }
6330
- function cleanJobPosting(raw) {
6331
- const source = typeof raw === "string" ? raw : "";
6332
- const pre = /<\/?[a-z][\s\S]*?>/i.test(source) ? stripHtml(source) : source;
6333
- const rawLines = pre.replace(/\r\n?/g, "\n").split("\n");
6334
- const lines = [];
6335
- for (const original of rawLines) {
6336
- const line = original.replace(/[ \t\f\v]+/g, " ").trim();
6337
- if (!line) {
6338
- if (lines.length && lines[lines.length - 1] !== "") lines.push("");
6339
- continue;
6340
- }
6341
- if (BOILERPLATE_RE.test(line)) continue;
6342
- if (/^[-=_*·•~]+$/.test(line)) continue;
6343
- lines.push(line);
6344
- }
6345
- while (lines.length && lines[0] === "") lines.shift();
6346
- while (lines.length && lines[lines.length - 1] === "") lines.pop();
6347
- const text = lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
6348
- const company = findLabelled(lines, COMPANY_LABEL_RE);
6349
- const position = findLabelled(lines, POSITION_LABEL_RE);
6350
- const deadline = findDeadline(text);
6351
- const keywords = extractTechKeywords(text);
6352
- const result = { text, keywords };
6353
- if (company) result.company = company;
6354
- if (position) result.position = position;
6355
- if (deadline) result.deadline = deadline;
6356
- return result;
5904
+ lines.push("");
5905
+ return lines.join("\n");
6357
5906
  }
6358
5907
 
6359
5908
  // packages/exporters/src/html.ts
6360
5909
  function escapeHtml(input) {
6361
5910
  return (input ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
6362
5911
  }
6363
- function looksLikeHtml2(s) {
6364
- return /<\/?(p|h[1-6]|ul|ol|li|div|section|article|br|hr|strong|em|a|table)\b/i.test(s);
6365
- }
6366
- function toPrintableHtml(title, bodyMarkdownOrHtml) {
6367
- const body = looksLikeHtml2(bodyMarkdownOrHtml) ? bodyMarkdownOrHtml : markdownToHtml(bodyMarkdownOrHtml);
5912
+ function toPrintableHtml(title, bodyMarkdown) {
5913
+ const body = markdownToHtml(bodyMarkdown);
6368
5914
  const safeTitle = escapeHtml(title);
6369
5915
  return `<!doctype html>
6370
5916
  <html lang="ko">
@@ -6453,22 +5999,10 @@ function slugify(input, fallback = "document") {
6453
5999
  function joinBlocks(blocks) {
6454
6000
  return blocks.filter((b) => typeof b === "string" && b.trim().length > 0).join("\n\n");
6455
6001
  }
6456
- function bulletList(items) {
6457
- const lines = items.filter((i) => typeof i === "string" && i.trim().length > 0).map((i) => `- ${i.trim()}`);
6458
- return lines.join("\n");
6459
- }
6460
6002
  function kv(label, value) {
6461
6003
  if (!value || !value.trim()) return "";
6462
6004
  return `**${label}**: ${value.trim()}`;
6463
6005
  }
6464
- function dateRange(start, end, current = false) {
6465
- const s = (start ?? "").trim();
6466
- const e = current ? "\uD604\uC7AC" : (end ?? "").trim();
6467
- if (!s && !e) return "";
6468
- if (!s) return e;
6469
- if (!e) return s;
6470
- return `${s} ~ ${e}`;
6471
- }
6472
6006
  function renderInline(escaped) {
6473
6007
  let out = escaped;
6474
6008
  out = out.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
@@ -6577,635 +6111,291 @@ function coverLetterToHtml(cl, opts) {
6577
6111
  content: toPrintableHtml(cl.title, coverLetterBody(cl, opts))
6578
6112
  };
6579
6113
  }
6580
- function resumeToMarkdown(doc, profile) {
6581
- const kindLabel = DOCUMENT_KIND_LABELS[doc.kind] ?? doc.kind;
6582
- const header = [];
6583
- if (profile?.name) header.push(kv("\uC774\uB984", profile.name));
6584
- if (profile?.email) header.push(kv("\uC774\uBA54\uC77C", profile.email));
6585
- if (profile?.phone) header.push(kv("\uC5F0\uB77D\uCC98", profile.phone));
6586
- const body = joinBlocks([
6587
- `# ${doc.title}`,
6588
- `_${kindLabel}_`,
6589
- header.length ? header.filter(Boolean).join("\n\n") : "",
6590
- header.length ? "---" : "",
6591
- (doc.content ?? "").trim() || "_(\uB0B4\uC6A9 \uC5C6\uC74C)_"
6592
- ]);
6593
- return {
6594
- filename: `${slugify(doc.title, doc.kind)}.md`,
6595
- mimeType: MD_MIME,
6596
- content: body
6597
- };
6598
- }
6599
- function profileToMarkdown(profile, experiences = [], projects = [], skills = []) {
6600
- const name = profile.name?.trim() || "\uC774\uB825\uC11C";
6601
- const contact = [];
6602
- if (profile.email) contact.push(profile.email);
6603
- if (profile.phone) contact.push(profile.phone);
6604
- if (profile.location) contact.push(profile.location);
6605
- const links = (profile.links ?? []).filter((l) => l.url).map((l) => `[${l.label || l.url}](${l.url})`);
6606
- const headerBlocks = joinBlocks([
6607
- `# ${name}`,
6608
- profile.headline ? `## ${profile.headline}` : "",
6609
- contact.length ? contact.join(" \xB7 ") : "",
6610
- links.length ? links.join(" \xB7 ") : ""
6611
- ]);
6612
- const summaryBlock = profile.summary?.trim() ? joinBlocks(["## \uC18C\uAC1C", profile.summary.trim()]) : "";
6613
- const desiredLines = [];
6614
- if (profile.desired_roles?.length) {
6615
- desiredLines.push(kv("\uD76C\uB9DD \uC9C1\uBB34", profile.desired_roles.join(", ")));
6616
- }
6617
- if (profile.desired_conditions?.trim()) {
6618
- desiredLines.push(kv("\uD76C\uB9DD \uC870\uAC74", profile.desired_conditions));
6619
- }
6620
- const desiredBlock = desiredLines.length ? joinBlocks(["## \uD76C\uB9DD \uC0AC\uD56D", desiredLines.filter(Boolean).join("\n\n")]) : "";
6621
- const expBlocks = experiences.length ? joinBlocks([
6622
- "## \uACBD\uB825",
6623
- ...experiences.map((e) => {
6624
- const range = dateRange(e.start_date, e.end_date, e.is_current);
6625
- const titleParts = [e.company, e.role].filter(Boolean).join(" \u2014 ");
6626
- const subParts = [e.employment_type, range].filter(Boolean).join(" \xB7 ");
6627
- return joinBlocks([
6628
- `### ${titleParts}`,
6629
- subParts ? `_${subParts}_` : "",
6630
- e.description?.trim() ? e.description.trim() : "",
6631
- e.achievements?.length ? bulletList(e.achievements) : "",
6632
- e.tech?.length ? kv("\uAE30\uC220", e.tech.join(", ")) : ""
6633
- ]);
6634
- })
6635
- ]) : "";
6636
- const projBlocks = projects.length ? joinBlocks([
6637
- "## \uD504\uB85C\uC81D\uD2B8",
6638
- ...projects.map((p) => {
6639
- const range = dateRange(p.start_date, p.end_date);
6640
- const subParts = [p.role, range].filter(Boolean).join(" \xB7 ");
6641
- return joinBlocks([
6642
- `### ${p.name}`,
6643
- subParts ? `_${subParts}_` : "",
6644
- p.description?.trim() ? p.description.trim() : "",
6645
- p.highlights?.length ? bulletList(p.highlights) : "",
6646
- p.tech?.length ? kv("\uAE30\uC220", p.tech.join(", ")) : "",
6647
- p.url ? kv("\uB9C1\uD06C", p.url) : ""
6648
- ]);
6649
- })
6650
- ]) : "";
6651
- let skillsBlock = "";
6652
- if (skills.length) {
6653
- const groups = /* @__PURE__ */ new Map();
6654
- for (const s of skills) {
6655
- const cat = s.category?.trim() || "\uAE30\uD0C0";
6656
- const label = s.level?.trim() ? `${s.name} (${s.level.trim()})` : s.name;
6657
- if (!groups.has(cat)) groups.set(cat, []);
6658
- groups.get(cat).push(label);
6659
- }
6660
- const lines = [...groups.entries()].map(
6661
- ([cat, items]) => kv(cat, items.join(", "))
6662
- );
6663
- skillsBlock = joinBlocks(["## \uBCF4\uC720 \uAE30\uC220", lines.filter(Boolean).join("\n\n")]);
6664
- }
6665
- const content = joinBlocks([
6666
- headerBlocks,
6667
- "---",
6668
- summaryBlock,
6669
- desiredBlock,
6670
- expBlocks,
6671
- projBlocks,
6672
- skillsBlock
6673
- ]);
6674
- return {
6675
- filename: `${slugify(name, "resume")}-resume.md`,
6676
- mimeType: MD_MIME,
6677
- content
6678
- };
6679
- }
6680
- function interviewPrepToMarkdown(prep, job) {
6681
- const titleBase = job ? `${job.company} ${job.position} \uBA74\uC811 \uC900\uBE44` : "\uBA74\uC811 \uC900\uBE44";
6682
- const meta = [];
6683
- if (job) {
6684
- meta.push(kv("\uD68C\uC0AC", job.company), kv("\uC9C1\uBB34", job.position));
6685
- if (job.deadline) meta.push(kv("\uB9C8\uAC10", job.deadline));
6686
- }
6687
- const introBlock = prep.self_introduction?.trim() ? joinBlocks(["## 1\uBD84 \uC790\uAE30\uC18C\uAC1C", prep.self_introduction.trim()]) : "";
6688
- const questionsBlock = prep.questions?.length ? joinBlocks([
6689
- "## \uC608\uC0C1 \uC9C8\uBB38",
6690
- ...prep.questions.map(
6691
- (q, idx) => joinBlocks([
6692
- `### Q${idx + 1}. ${q.question}`,
6693
- q.intent?.trim() ? kv("\uC758\uB3C4", q.intent) : "",
6694
- q.answer_outline?.trim() ? joinBlocks(["**\uB2F5\uBCC0 \uAC00\uC774\uB4DC**", q.answer_outline.trim()]) : "",
6695
- q.followups?.length ? joinBlocks(["**\uC608\uC0C1 \uAF2C\uB9AC \uC9C8\uBB38**", bulletList(q.followups)]) : ""
6696
- ])
6697
- )
6698
- ]) : "";
6699
- const starBlock = prep.star_guides?.length ? joinBlocks([
6700
- "## STAR \uC815\uB9AC",
6701
- ...prep.star_guides.map(
6702
- (s) => joinBlocks([
6703
- `### ${s.question}`,
6704
- kv("Situation", s.situation),
6705
- kv("Task", s.task),
6706
- kv("Action", s.action),
6707
- kv("Result", s.result)
6708
- ])
6709
- )
6710
- ]) : "";
6711
- const notesBlock = prep.notes?.trim() ? joinBlocks(["## \uBA54\uBAA8", prep.notes.trim()]) : "";
6712
- const content = joinBlocks([
6713
- `# ${titleBase}`,
6714
- meta.length ? meta.filter(Boolean).join("\n\n") : "",
6715
- meta.length ? "---" : "",
6716
- introBlock,
6717
- questionsBlock,
6718
- starBlock,
6719
- notesBlock
6720
- ]);
6721
- return {
6722
- filename: `${slugify(titleBase, "interview-prep")}.md`,
6723
- mimeType: MD_MIME,
6724
- content
6725
- };
6726
- }
6727
6114
 
6728
- // apps/web/src/exports.ts
6729
- function asFormat(result, format) {
6730
- if (format === "txt") {
6731
- return {
6732
- filename: result.filename.replace(/\.(md|html)$/i, ".txt"),
6733
- mimeType: "text/plain; charset=utf-8",
6734
- content: result.content
6735
- };
6736
- }
6737
- return result;
6738
- }
6739
- function exportCoverLetter(id3, format) {
6740
- const cl = coverLetterRepo.get(id3, true);
6741
- if (!cl) throw new HttpError(404, "\uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
6742
- const job = cl.job_id ? jobRepo.get(cl.job_id) : null;
6743
- const profile = profileRepo.get();
6744
- if (format === "html") return coverLetterToHtml(cl, { job, profile });
6745
- return asFormat(coverLetterToMarkdown(cl, { job, profile }), format);
6746
- }
6747
- function exportDocument(id3, format) {
6748
- const doc = documentRepo.get(id3);
6749
- if (!doc) throw new HttpError(404, "\uBB38\uC11C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
6750
- return asFormat(resumeToMarkdown(doc, profileRepo.get()), format);
6751
- }
6752
- function exportProfile(format) {
6753
- const profile = profileRepo.get();
6754
- if (!profile) throw new HttpError(404, "\uD504\uB85C\uD544\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
6755
- return asFormat(
6756
- profileToMarkdown(profile, experienceRepo.list(), projectRepo.list(), skillRepo.list()),
6757
- format
6758
- );
6759
- }
6760
- function exportInterview(jobId, format) {
6761
- const prep = interviewRepo.getByJob(jobId);
6762
- if (!prep) throw new HttpError(404, "\uBA74\uC811 \uC900\uBE44 \uC790\uB8CC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
6763
- return asFormat(interviewPrepToMarkdown(prep, jobRepo.get(jobId)), format);
6764
- }
6765
-
6766
- // apps/web/src/info.ts
6767
- var APP_NAME = "CareerMate";
6768
- var APP_VERSION = "0.1.0";
6769
- var _port = 0;
6770
- function setServerPort(port) {
6771
- _port = port;
6772
- }
6773
- function getServerInfo() {
6774
- return {
6775
- name: APP_NAME,
6776
- version: APP_VERSION,
6777
- port: _port,
6778
- url: `http://127.0.0.1:${_port}`,
6779
- data_dir: getDataDir(),
6780
- db_path: getDbPath(),
6781
- exports_dir: getExportsDir(),
6782
- backups_dir: getBackupsDir(),
6783
- 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
- }
6795
- };
6115
+ // packages/parsers/src/html.ts
6116
+ var NAMED_ENTITIES = {
6117
+ amp: "&",
6118
+ lt: "<",
6119
+ gt: ">",
6120
+ quot: '"',
6121
+ apos: "'",
6122
+ nbsp: " ",
6123
+ middot: "\xB7",
6124
+ hellip: "\u2026",
6125
+ mdash: "\u2014",
6126
+ ndash: "\u2013",
6127
+ bull: "\u2022",
6128
+ copy: "\xA9",
6129
+ reg: "\xAE",
6130
+ trade: "\u2122"
6131
+ };
6132
+ function decodeEntities(input) {
6133
+ return input.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (match, body) => {
6134
+ if (body[0] === "#") {
6135
+ const isHex = body[1] === "x" || body[1] === "X";
6136
+ const code = parseInt(body.slice(isHex ? 2 : 1), isHex ? 16 : 10);
6137
+ if (Number.isFinite(code) && code > 0 && code <= 1114111) {
6138
+ try {
6139
+ return String.fromCodePoint(code);
6140
+ } catch {
6141
+ return match;
6142
+ }
6143
+ }
6144
+ return match;
6145
+ }
6146
+ const named = NAMED_ENTITIES[body.toLowerCase()];
6147
+ return named !== void 0 ? named : match;
6148
+ });
6149
+ }
6150
+ var BLOCK_TAG = /<\/?(p|div|section|article|header|footer|li|ul|ol|tr|table|h[1-6]|blockquote|pre)\b[^>]*>/gi;
6151
+ function stripHtml(html) {
6152
+ if (!html) return "";
6153
+ let s = String(html);
6154
+ s = s.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, " ");
6155
+ s = s.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, " ");
6156
+ s = s.replace(/<!--[\s\S]*?-->/g, " ");
6157
+ s = s.replace(/<br\b[^>]*>/gi, "\n");
6158
+ s = s.replace(/<hr\b[^>]*>/gi, "\n");
6159
+ s = s.replace(BLOCK_TAG, "\n");
6160
+ s = s.replace(/<\/?[^>]+>/g, " ");
6161
+ s = decodeEntities(s);
6162
+ s = s.replace(/[ \t\f\v]+/g, " ");
6163
+ s = s.split("\n").map((line) => line.trim()).join("\n");
6164
+ s = s.replace(/\n{3,}/g, "\n\n");
6165
+ return s.trim();
6796
6166
  }
6797
6167
 
6798
- // apps/web/src/settings.ts
6799
- import fs4 from "node:fs";
6800
- import path4 from "node:path";
6801
- var TABLES = [
6802
- "profile",
6803
- "experiences",
6804
- "projects",
6805
- "skills",
6806
- "documents",
6807
- "cover_letters",
6808
- "cover_letter_versions",
6809
- "jobs",
6810
- "fit_analyses",
6811
- "applications",
6812
- "interview_preps",
6813
- "activities"
6168
+ // packages/parsers/src/keywords.ts
6169
+ var TECH_TERMS = [
6170
+ // Languages
6171
+ { label: "JavaScript", aliases: ["js"] },
6172
+ { label: "TypeScript", aliases: ["ts"] },
6173
+ { label: "Python" },
6174
+ { label: "Java" },
6175
+ { label: "Kotlin" },
6176
+ { label: "Go", aliases: ["golang"] },
6177
+ { label: "Rust" },
6178
+ { label: "C++", aliases: ["cpp"] },
6179
+ { label: "C#", aliases: ["csharp"] },
6180
+ { label: "C" },
6181
+ { label: "Ruby" },
6182
+ { label: "PHP" },
6183
+ { label: "Swift" },
6184
+ { label: "Scala" },
6185
+ { label: "Dart" },
6186
+ { label: "SQL" },
6187
+ { label: "GraphQL" },
6188
+ // Frontend frameworks/libs
6189
+ { label: "React", aliases: ["react.js", "reactjs"] },
6190
+ { label: "Next.js", aliases: ["nextjs", "next js"] },
6191
+ { label: "Vue", aliases: ["vue.js", "vuejs"] },
6192
+ { label: "Nuxt", aliases: ["nuxt.js", "nuxtjs"] },
6193
+ { label: "Angular" },
6194
+ { label: "Svelte" },
6195
+ { label: "Redux" },
6196
+ { label: "Tailwind", aliases: ["tailwindcss", "tailwind css"] },
6197
+ // Backend frameworks
6198
+ { label: "Node.js", aliases: ["nodejs", "node js", "node"] },
6199
+ { label: "Express", aliases: ["express.js", "expressjs"] },
6200
+ { label: "NestJS", aliases: ["nest.js", "nest js"] },
6201
+ { label: "Spring", aliases: ["spring boot", "springboot"] },
6202
+ { label: "Django" },
6203
+ { label: "Flask" },
6204
+ { label: "FastAPI" },
6205
+ { label: "Rails", aliases: ["ruby on rails"] },
6206
+ { label: "Laravel" },
6207
+ { label: ".NET", aliases: ["dotnet", "asp.net"] },
6208
+ // Datastores
6209
+ { label: "PostgreSQL", aliases: ["postgres", "postgresql"] },
6210
+ { label: "MySQL" },
6211
+ { label: "MariaDB" },
6212
+ { label: "MongoDB", aliases: ["mongo"] },
6213
+ { label: "Redis" },
6214
+ { label: "Elasticsearch", aliases: ["elastic search"] },
6215
+ { label: "Oracle" },
6216
+ { label: "SQLite" },
6217
+ { label: "Kafka" },
6218
+ { label: "RabbitMQ" },
6219
+ // Cloud / infra / devops
6220
+ { label: "AWS", aliases: ["amazon web services"] },
6221
+ { label: "GCP", aliases: ["google cloud"] },
6222
+ { label: "Azure" },
6223
+ { label: "Docker" },
6224
+ { label: "Kubernetes", aliases: ["k8s"] },
6225
+ { label: "Terraform" },
6226
+ { label: "Jenkins" },
6227
+ { label: "GitHub Actions", aliases: ["github action"] },
6228
+ { label: "CI/CD", aliases: ["cicd"] },
6229
+ { label: "Nginx" },
6230
+ { label: "Linux" },
6231
+ // Tooling / misc
6232
+ { label: "Git" },
6233
+ { label: "REST", aliases: ["rest api", "restful"] },
6234
+ { label: "gRPC" },
6235
+ { label: "Webpack" },
6236
+ { label: "Vite" },
6237
+ { label: "Jest" },
6238
+ { label: "Figma" },
6239
+ { label: "Jira" },
6240
+ // Data / ML
6241
+ { label: "TensorFlow" },
6242
+ { label: "PyTorch" },
6243
+ { label: "Pandas" },
6244
+ { label: "NumPy", aliases: ["numpy"] },
6245
+ { label: "Spark", aliases: ["apache spark"] },
6246
+ { label: "Airflow" }
6814
6247
  ];
6815
- function exportAll() {
6816
- const db = getDb();
6817
- const tables = {};
6818
- for (const t of TABLES) {
6819
- tables[t] = db.prepare(`SELECT * FROM ${t}`).all();
6820
- }
6821
- const ver = db.prepare(`SELECT value FROM _meta WHERE key='schema_version'`).get();
6822
- return { exported_at: (/* @__PURE__ */ new Date()).toISOString(), version: Number(ver?.value ?? 0), tables };
6248
+ function escapeRegExp(s) {
6249
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6823
6250
  }
6824
- function createBackup() {
6825
- const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
6826
- const dir = getBackupsDir();
6827
- const dbBackup = path4.join(dir, `careermate-${stamp}.sqlite`);
6828
- const jsonBackup = path4.join(dir, `careermate-${stamp}.json`);
6829
- try {
6830
- getDb().exec(`VACUUM INTO '${dbBackup.replace(/'/g, "''")}'`);
6831
- } catch {
6832
- fs4.copyFileSync(getDbPath(), dbBackup);
6251
+ function buildPattern(term) {
6252
+ const esc = escapeRegExp(term);
6253
+ if (/^[\w.]+$/.test(term) && /\w$/.test(term) && /^\w/.test(term)) {
6254
+ return new RegExp(`(?<![\\w])${esc}(?![\\w])`, "i");
6833
6255
  }
6834
- fs4.writeFileSync(jsonBackup, JSON.stringify(exportAll(), null, 2), "utf8");
6835
- return { backup_path: dbBackup, json_path: jsonBackup };
6256
+ return new RegExp(`(?<![A-Za-z0-9])${esc}(?![A-Za-z0-9])`, "i");
6836
6257
  }
6837
- function resetAll(confirm) {
6838
- if (confirm !== "DELETE") {
6839
- return { ok: false };
6840
- }
6841
- const { backup_path } = createBackup();
6842
- const db = getDb();
6843
- db.exec("BEGIN");
6844
- try {
6845
- for (const t of TABLES) db.exec(`DELETE FROM ${t}`);
6846
- db.exec("COMMIT");
6847
- } catch (e) {
6848
- db.exec("ROLLBACK");
6849
- throw e;
6258
+ function extractTechKeywords(text, limit = 20) {
6259
+ if (!text) return [];
6260
+ const found = [];
6261
+ const seen = /* @__PURE__ */ new Set();
6262
+ for (const term of TECH_TERMS) {
6263
+ if (seen.has(term.label)) continue;
6264
+ const candidates = [term.label, ...term.aliases ?? []];
6265
+ const matched = candidates.some((c) => {
6266
+ try {
6267
+ return buildPattern(c).test(text);
6268
+ } catch {
6269
+ return false;
6270
+ }
6271
+ });
6272
+ if (matched) {
6273
+ seen.add(term.label);
6274
+ found.push(term.label);
6275
+ if (found.length >= limit) break;
6276
+ }
6850
6277
  }
6851
- return { ok: true, backup_path };
6852
- }
6853
- function listBackups() {
6854
- const dir = getBackupsDir();
6855
- if (!fs4.existsSync(dir)) return [];
6856
- return fs4.readdirSync(dir).filter((f) => f.endsWith(".sqlite") || f.endsWith(".json")).map((f) => {
6857
- const p = path4.join(dir, f);
6858
- const st = fs4.statSync(p);
6859
- return { filename: f, path: p, size: st.size, created_at: st.mtime.toISOString() };
6860
- }).sort((a, b) => b.created_at.localeCompare(a.created_at));
6278
+ return found;
6861
6279
  }
6862
6280
 
6863
- // apps/web/src/routes.ts
6864
- function id2(ctx, key = "id") {
6865
- const v = ctx.params[key];
6866
- if (!v) throw new HttpError(400, "id\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.");
6867
- return v;
6281
+ // packages/parsers/src/index.ts
6282
+ var BOILERPLATE_RE = /^(home|menu|login|로그인|회원가입|sign\s?up|sign\s?in|share|공유|스크랩|북마크|이전|다음|목록|prev(ious)?|next|cookie|쿠키|copyright|©|all rights reserved|이용약관|개인정보처리방침|상단으로|top|search|검색)$/i;
6283
+ var COMPANY_LABEL_RE = /(?:회사명?|기업명?|company)\s*[::]\s*(.+)/i;
6284
+ var POSITION_LABEL_RE = /(?:포지션|직무|모집\s*부문|채용\s*부문|position|job\s*title)\s*[::]\s*(.+)/i;
6285
+ var DEADLINE_LABEL_RE = /(?:마감(?:일|일자|기한)?|접수\s*마감|deadline|due)\s*[::]?\s*(.+)/i;
6286
+ function findDeadline(text) {
6287
+ const rolling = /(상시\s*채용|채용\s*시\s*(?:마감|까지)|수시\s*채용)/i.exec(text);
6288
+ for (const line of text.split("\n")) {
6289
+ const m = DEADLINE_LABEL_RE.exec(line);
6290
+ if (m) {
6291
+ const date = normalizeDate(m[1]);
6292
+ if (date) return date;
6293
+ const tail = m[1].trim();
6294
+ if (tail && tail.length <= 30) return tail;
6295
+ }
6296
+ }
6297
+ const any = normalizeDate(text);
6298
+ if (any) return any;
6299
+ if (rolling) return rolling[1].replace(/\s+/g, " ").trim();
6300
+ return void 0;
6868
6301
  }
6869
- function fmt(ctx) {
6870
- const f = (ctx.query.get("format") ?? "md").toLowerCase();
6871
- return ["md", "html", "txt"].includes(f) ? f : "md";
6302
+ function normalizeDate(s) {
6303
+ if (!s) return void 0;
6304
+ let m = /(\d{4})[.\-/년]\s*(\d{1,2})[.\-/월]\s*(\d{1,2})/.exec(s);
6305
+ if (m) {
6306
+ const [, y, mo, d] = m;
6307
+ return `${y}-${mo.padStart(2, "0")}-${d.padStart(2, "0")}`;
6308
+ }
6309
+ m = /(\d{4})[.\-/년]\s*(\d{1,2})\b/.exec(s);
6310
+ if (m) {
6311
+ const [, y, mo] = m;
6312
+ return `${y}-${mo.padStart(2, "0")}`;
6313
+ }
6314
+ return void 0;
6872
6315
  }
6873
- function jobDetail(jobId) {
6874
- const job = jobRepo.get(jobId);
6875
- if (!job) throw new HttpError(404, "\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
6876
- const application = applicationRepo.getByJob(jobId);
6877
- const fit = fitRepo.getByJob(jobId);
6878
- const cover_letters = coverLetterRepo.listByJob(jobId);
6879
- const interview = interviewRepo.getByJob(jobId);
6880
- const related = jobRepo.relatedTo(job.company, job.position, job.id).map(jobWithMeta);
6881
- return {
6882
- ...jobWithMeta(job),
6883
- application,
6884
- fit,
6885
- cover_letters,
6886
- interview,
6887
- related
6888
- };
6316
+ function findLabelled(lines, re) {
6317
+ for (const line of lines) {
6318
+ const m = re.exec(line);
6319
+ if (m) {
6320
+ const val = m[1].trim();
6321
+ if (val && val.length <= 80) return val;
6322
+ }
6323
+ }
6324
+ return void 0;
6889
6325
  }
6890
- function registerApiRoutes(router2) {
6891
- router2.get("/api/health", () => ({ ok: true, ...getServerInfo() }));
6892
- router2.get("/api/onboarding", () => getOnboardingStatus());
6893
- router2.get("/api/summary", () => getHomeSummary());
6894
- router2.get("/api/activity", (ctx) => ({
6895
- activities: listRecentActivity(Number(ctx.query.get("limit") ?? 30))
6896
- }));
6897
- router2.get(
6898
- "/api/context",
6899
- (ctx) => getApplicationContext({ job_id: ctx.query.get("job_id") ?? void 0 })
6900
- );
6901
- router2.get("/api/prompts", () => ({ prompts: PROMPTS }));
6902
- router2.get("/api/workflows", () => ({ workflows: WORKFLOWS }));
6903
- router2.get("/api/meta", () => ({
6904
- statuses: APPLICATION_BOARD_ORDER.map((s) => ({ value: s, label: APPLICATION_STATUS_LABELS[s] })),
6905
- document_kinds: Object.entries(DOCUMENT_KIND_LABELS).map(([value, label]) => ({ value, label }))
6906
- }));
6907
- router2.get("/api/profile", () => ({ profile: profileRepo.get() }));
6908
- router2.put("/api/profile", async (ctx) => {
6909
- const input = await readJsonBody(ctx.req, ProfileInputSchema);
6910
- return { profile: saveProfile(input) };
6911
- });
6912
- router2.get("/api/experiences", () => ({ experiences: experienceRepo.list() }));
6913
- router2.post("/api/experiences", async (ctx) => {
6914
- const input = await readJsonBody(ctx.req, ExperienceInputSchema);
6915
- const exp = experienceRepo.add(input);
6916
- activityRepo.log("profile_updated", `\uACBD\uB825(${exp.company})\uC744 \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "experience", exp.id);
6917
- return { experience: exp };
6918
- });
6919
- router2.put("/api/experiences/:id", async (ctx) => {
6920
- const input = await readJsonBody(ctx.req, ExperienceInputSchema.partial());
6921
- const exp = experienceRepo.update(id2(ctx), input);
6922
- if (!exp) throw new HttpError(404, "\uACBD\uB825\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
6923
- return { experience: exp };
6924
- });
6925
- router2.delete("/api/experiences/:id", (ctx) => ({ ok: experienceRepo.remove(id2(ctx)) }));
6926
- router2.get("/api/projects", () => ({ projects: projectRepo.list() }));
6927
- router2.post("/api/projects", async (ctx) => {
6928
- const input = await readJsonBody(ctx.req, ProjectInputSchema);
6929
- const prj = projectRepo.add(input);
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 };
6932
- });
6933
- router2.put("/api/projects/:id", async (ctx) => {
6934
- const input = await readJsonBody(ctx.req, ProjectInputSchema.partial());
6935
- const prj = projectRepo.update(id2(ctx), input);
6936
- if (!prj) throw new HttpError(404, "\uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
6937
- return { project: prj };
6938
- });
6939
- router2.delete("/api/projects/:id", (ctx) => ({ ok: projectRepo.remove(id2(ctx)) }));
6940
- router2.get("/api/skills", () => ({ skills: skillRepo.list() }));
6941
- router2.post("/api/skills", async (ctx) => {
6942
- const input = await readJsonBody(ctx.req, SkillInputSchema);
6943
- return { skill: skillRepo.add(input) };
6944
- });
6945
- router2.put("/api/skills/:id", async (ctx) => {
6946
- const input = await readJsonBody(ctx.req, SkillInputSchema.partial());
6947
- const skill = skillRepo.update(id2(ctx), input);
6948
- if (!skill) throw new HttpError(404, "\uAE30\uC220\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
6949
- return { skill };
6950
- });
6951
- router2.delete("/api/skills/:id", (ctx) => ({ ok: skillRepo.remove(id2(ctx)) }));
6952
- router2.get("/api/documents", (ctx) => {
6953
- const kind = ctx.query.get("kind");
6954
- return { documents: documentRepo.list(kind ?? void 0) };
6955
- });
6956
- router2.post("/api/documents", async (ctx) => {
6957
- const input = await readJsonBody(ctx.req, DocumentInputSchema);
6958
- return { document: addResume(input) };
6959
- });
6960
- router2.get("/api/documents/:id", (ctx) => {
6961
- const doc = documentRepo.get(id2(ctx));
6962
- if (!doc) throw new HttpError(404, "\uBB38\uC11C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
6963
- return { document: doc };
6964
- });
6965
- router2.put("/api/documents/:id", async (ctx) => {
6966
- const input = await readJsonBody(ctx.req, DocumentInputSchema.partial());
6967
- const doc = documentRepo.update(id2(ctx), input);
6968
- if (!doc) throw new HttpError(404, "\uBB38\uC11C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
6969
- return { document: doc };
6970
- });
6971
- router2.delete("/api/documents/:id", (ctx) => ({ ok: documentRepo.remove(id2(ctx)) }));
6972
- router2.get("/api/cover-letters", () => ({ cover_letters: coverLetterRepo.list() }));
6973
- router2.post("/api/cover-letters", async (ctx) => {
6974
- const input = await readJsonBody(ctx.req, CoverLetterInputSchema);
6975
- if (input.content) {
6976
- const { coverLetter } = saveCoverLetterVersion({
6977
- title: input.title,
6978
- job_id: input.job_id,
6979
- content: input.content,
6980
- note: input.note,
6981
- source: input.source ?? "manual"
6982
- });
6983
- if (input.is_primary) coverLetterRepo.setPrimary(coverLetter.id);
6984
- activityRepo.log("cover_letter_added", `${coverLetter.title} \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "cover_letter", coverLetter.id);
6985
- return { cover_letter: coverLetterRepo.get(coverLetter.id) };
6326
+ function cleanJobPosting(raw) {
6327
+ const source = typeof raw === "string" ? raw : "";
6328
+ const pre = /<\/?[a-z][\s\S]*?>/i.test(source) ? stripHtml(source) : source;
6329
+ const rawLines = pre.replace(/\r\n?/g, "\n").split("\n");
6330
+ const lines = [];
6331
+ for (const original of rawLines) {
6332
+ const line = original.replace(/[ \t\f\v]+/g, " ").trim();
6333
+ if (!line) {
6334
+ if (lines.length && lines[lines.length - 1] !== "") lines.push("");
6335
+ continue;
6986
6336
  }
6987
- const cl = coverLetterRepo.create({ title: input.title, job_id: input.job_id ?? null, is_primary: input.is_primary });
6988
- activityRepo.log("cover_letter_added", `${cl.title} \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "cover_letter", cl.id);
6989
- return { cover_letter: cl };
6990
- });
6991
- router2.get("/api/cover-letters/:id", (ctx) => {
6992
- const cl = coverLetterRepo.get(id2(ctx), true);
6993
- if (!cl) throw new HttpError(404, "\uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
6994
- return { cover_letter: cl };
6995
- });
6996
- router2.post("/api/cover-letters/:id/versions", async (ctx) => {
6997
- const input = await readJsonBody(
6998
- ctx.req,
6999
- CoverLetterVersionInputSchema.omit({ cover_letter_id: true })
7000
- );
7001
- const { coverLetter, version } = saveCoverLetterVersion({ ...input, cover_letter_id: id2(ctx) });
7002
- return { cover_letter: coverLetter, version };
7003
- });
7004
- router2.put("/api/cover-letters/:id/current-version", async (ctx) => {
7005
- const { version_id } = await readJsonBody(ctx.req, external_exports.object({ version_id: external_exports.string() }));
7006
- const cl = coverLetterRepo.setCurrentVersion(id2(ctx), version_id);
7007
- if (!cl) throw new HttpError(404, "\uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
7008
- return { cover_letter: cl };
7009
- });
7010
- router2.put("/api/cover-letters/:id/primary", (ctx) => {
7011
- const cl = coverLetterRepo.setPrimary(id2(ctx));
7012
- if (!cl) throw new HttpError(404, "\uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
7013
- return { cover_letter: cl };
7014
- });
7015
- router2.delete("/api/cover-letters/:id", (ctx) => ({ ok: coverLetterRepo.remove(id2(ctx)) }));
7016
- router2.get("/api/jobs", () => ({ jobs: jobRepo.list().map(jobWithMeta) }));
7017
- router2.post("/api/jobs", async (ctx) => {
7018
- const input = await readJsonBody(ctx.req, JobInputSchema);
7019
- const { job, application } = saveJobPosting(input);
7020
- return { job: jobWithMeta(job), application };
7021
- });
7022
- router2.get("/api/jobs/:id", (ctx) => ({ job: jobDetail(id2(ctx)) }));
7023
- router2.put("/api/jobs/:id", async (ctx) => {
7024
- const input = await readJsonBody(ctx.req, JobInputSchema.partial());
7025
- const existing = jobRepo.get(id2(ctx));
7026
- if (!existing) throw new HttpError(404, "\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
7027
- const job = jobRepo.upsert({ ...existing, ...input }, existing.id);
7028
- return { job: jobWithMeta(job) };
7029
- });
7030
- router2.delete("/api/jobs/:id", (ctx) => ({ ok: jobRepo.remove(id2(ctx)) }));
7031
- router2.put("/api/jobs/:id/fit", async (ctx) => {
7032
- const input = await readJsonBody(ctx.req, FitAnalysisInputSchema.omit({ job_id: true }));
7033
- return { fit: saveFitAnalysis({ ...input, job_id: id2(ctx) }) };
7034
- });
7035
- router2.get("/api/interview", () => {
7036
- const preps = interviewRepo.list().map((p) => ({ ...p, job: jobRepo.get(p.job_id) }));
7037
- const eligible = applicationRepo.list().filter((a) => ["document_passed", "interview", "final_passed"].includes(a.status)).map((a) => ({
7038
- job: jobRepo.get(a.job_id),
7039
- status: a.status,
7040
- status_label: APPLICATION_STATUS_LABELS[a.status],
7041
- has_prep: !!interviewRepo.getByJob(a.job_id)
7042
- })).filter((x) => x.job);
7043
- return { preps, eligible };
7044
- });
7045
- router2.get("/api/jobs/:id/interview", (ctx) => ({ interview: interviewRepo.getByJob(id2(ctx)) }));
7046
- router2.put("/api/jobs/:id/interview", async (ctx) => {
7047
- const input = await readJsonBody(ctx.req, InterviewPrepInputSchema.omit({ job_id: true }));
7048
- return { interview: saveInterviewPrep({ ...input, job_id: id2(ctx) }) };
7049
- });
7050
- router2.get("/api/applications", () => {
7051
- const apps = applicationRepo.list().map((a) => ({
7052
- ...a,
7053
- status_label: APPLICATION_STATUS_LABELS[a.status],
7054
- job: jobRepo.get(a.job_id),
7055
- fit_score: fitRepo.getByJob(a.job_id)?.score ?? null
7056
- }));
7057
- return { applications: apps, board_order: APPLICATION_BOARD_ORDER };
7058
- });
7059
- router2.put("/api/applications/:jobId/status", async (ctx) => {
7060
- const body = await readJsonBody(ctx.req, ApplicationStatusUpdateSchema.omit({ job_id: true, application_id: true }));
7061
- return updateApplicationStatus(id2(ctx, "jobId"), body.status, body.note);
7062
- });
7063
- router2.put("/api/applications/:jobId", async (ctx) => {
7064
- const body = await readJsonBody(ctx.req, ApplicationInputSchema.omit({ job_id: true }));
7065
- return { application: applicationRepo.upsert({ ...body, job_id: id2(ctx, "jobId") }) };
7066
- });
7067
- router2.get("/api/export/cover-letter/:id", (ctx) => download(exportCoverLetter(id2(ctx), fmt(ctx))));
7068
- router2.get("/api/export/document/:id", (ctx) => download(exportDocument(id2(ctx), fmt(ctx))));
7069
- router2.get("/api/export/profile", (ctx) => download(exportProfile(fmt(ctx))));
7070
- router2.get("/api/export/interview/:jobId", (ctx) => download(exportInterview(id2(ctx, "jobId"), fmt(ctx))));
7071
- router2.get("/api/settings", () => ({
7072
- info: getServerInfo(),
7073
- backups: listBackups()
7074
- }));
7075
- router2.get(
7076
- "/api/settings/export-all",
7077
- () => download({
7078
- filename: `careermate-backup-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.json`,
7079
- mimeType: "application/json; charset=utf-8",
7080
- content: JSON.stringify(exportAll(), null, 2)
7081
- })
7082
- );
7083
- router2.post("/api/settings/backup", () => createBackup());
7084
- router2.post("/api/settings/reset", async (ctx) => {
7085
- const { confirm } = await readJsonBody(ctx.req, external_exports.object({ confirm: external_exports.string() }));
7086
- const result = resetAll(confirm);
7087
- if (!result.ok) throw new HttpError(400, '\uCD08\uAE30\uD654\uB97C \uC9C4\uD589\uD558\uB824\uBA74 confirm \uAC12\uC73C\uB85C "DELETE"\uB97C \uBCF4\uB0B4\uC57C \uD569\uB2C8\uB2E4.');
7088
- activityRepo.log("profile_updated", "\uBAA8\uB4E0 \uB370\uC774\uD130\uB97C \uCD08\uAE30\uD654\uD588\uC2B5\uB2C8\uB2E4(\uBC31\uC5C5 \uC0DD\uC131\uB428).");
7089
- return result;
7090
- });
7091
- router2.post("/api/parse/job", async (ctx) => {
7092
- const { raw } = await readJsonBody(ctx.req, external_exports.object({ raw: external_exports.string() }));
7093
- return cleanJobPosting(raw);
7094
- });
7095
- router2.post("/api/parse/text", async (ctx) => {
7096
- const body = await readJsonBody(
7097
- ctx.req,
7098
- external_exports.object({ filename: external_exports.string().optional(), mimeType: external_exports.string().optional(), content: external_exports.string() })
7099
- );
7100
- return extractText(body);
7101
- });
6337
+ if (BOILERPLATE_RE.test(line)) continue;
6338
+ if (/^[-=_*·•~]+$/.test(line)) continue;
6339
+ lines.push(line);
6340
+ }
6341
+ while (lines.length && lines[0] === "") lines.shift();
6342
+ while (lines.length && lines[lines.length - 1] === "") lines.pop();
6343
+ const text = lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
6344
+ const company = findLabelled(lines, COMPANY_LABEL_RE);
6345
+ const position = findLabelled(lines, POSITION_LABEL_RE);
6346
+ const deadline = findDeadline(text);
6347
+ const keywords = extractTechKeywords(text);
6348
+ const result = { text, keywords };
6349
+ if (company) result.company = company;
6350
+ if (position) result.position = position;
6351
+ if (deadline) result.deadline = deadline;
6352
+ return result;
7102
6353
  }
7103
- function download(result) {
7104
- return { __download: result };
6354
+
6355
+ // packages/mcp-tools/src/result.ts
6356
+ function ok(text, data) {
6357
+ return { text, data };
7105
6358
  }
7106
- function isDownload(v) {
7107
- return typeof v === "object" && v !== null && "__download" in v;
6359
+ function fail(text) {
6360
+ return { text, isError: true };
7108
6361
  }
7109
6362
 
7110
- // apps/web/src/server.ts
6363
+ // packages/mcp-tools/src/bridge.ts
6364
+ import { spawn } from "node:child_process";
6365
+ import path3 from "node:path";
6366
+ import { fileURLToPath } from "node:url";
7111
6367
  var BUNDLED = true;
7112
- var __dirname = path5.dirname(fileURLToPath(import.meta.url));
7113
- var WEB_ROOT = BUNDLED ? path5.resolve(__dirname, "public") : path5.resolve(__dirname, "..", "public");
7114
- var INSTALL_ROOT = BUNDLED ? path5.resolve(__dirname, "install-page") : path5.resolve(__dirname, "..", "..", "..", "install-page");
7115
- var router = new Router();
7116
- registerApiRoutes(router);
7117
- function createServer() {
7118
- return http.createServer(async (req, res) => {
7119
- try {
7120
- const decision = checkRequest(req);
7121
- if (!decision.ok) {
7122
- sendJson(res, decision.status ?? 403, { error: "\uC811\uADFC\uC774 \uAC70\uBD80\uB418\uC5C8\uC2B5\uB2C8\uB2E4.", code: decision.reason });
7123
- return;
7124
- }
7125
- const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
7126
- const pathname = url.pathname;
7127
- if (pathname.startsWith("/api/")) {
7128
- const matched = router.match(req.method ?? "GET", pathname);
7129
- if (!matched) {
7130
- sendJson(res, 404, { error: "\uC874\uC7AC\uD558\uC9C0 \uC54A\uB294 API \uACBD\uB85C\uC785\uB2C8\uB2E4.", path: pathname });
7131
- return;
7132
- }
7133
- const result = await matched.handler({
7134
- req,
7135
- res,
7136
- params: matched.params,
7137
- query: url.searchParams,
7138
- url
7139
- });
7140
- if (res.writableEnded) return;
7141
- if (isDownload(result)) {
7142
- const d = result.__download;
7143
- sendDownload(res, d.filename, d.mimeType, d.content);
7144
- } else {
7145
- sendJson(res, 200, result ?? { ok: true });
7146
- }
7147
- return;
7148
- }
7149
- if (pathname === "/install") {
7150
- res.writeHead(308, { Location: `/install/${url.search}` });
7151
- res.end();
7152
- return;
7153
- }
7154
- if (pathname.startsWith("/install/")) {
7155
- const rel2 = pathname === "/install/" ? "index.html" : pathname.slice("/install/".length);
7156
- if (serveStatic(res, INSTALL_ROOT, rel2, injectToken)) return;
7157
- if (serveStatic(res, INSTALL_ROOT, "index.html", injectToken)) return;
7158
- }
7159
- const rel = pathname === "/" ? "index.html" : pathname.slice(1);
7160
- if (serveStatic(res, WEB_ROOT, rel, injectToken)) return;
7161
- if (serveStatic(res, WEB_ROOT, "index.html", injectToken)) return;
7162
- sendJson(res, 404, { error: "Not found" });
7163
- } catch (err) {
7164
- if (err instanceof HttpError) {
7165
- sendJson(res, err.status, { error: err.message, code: err.code });
7166
- } else {
7167
- const message = err instanceof Error ? err.message : "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958";
7168
- console.error("[careermate] \uC694\uCCAD \uCC98\uB9AC \uC911 \uC624\uB958:", message);
7169
- sendJson(res, 500, { error: "\uC11C\uBC84 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4." });
7170
- }
7171
- }
7172
- });
6368
+ var __dirname = path3.dirname(fileURLToPath(import.meta.url));
6369
+ var REPO_ROOT = path3.resolve(__dirname, "..", "..", "..");
6370
+ function liveServer() {
6371
+ const info2 = readRuntimeInfo();
6372
+ if (info2 && isProcessAlive(info2.pid)) return info2;
6373
+ return null;
7173
6374
  }
7174
- function startServer(preferredPort) {
7175
- getDb();
7176
- const server = createServer();
7177
- return new Promise((resolve, reject) => {
7178
- let attempt = 0;
7179
- const tryListen = (port) => {
7180
- server.once("error", (e) => {
7181
- if (e.code === "EADDRINUSE" && attempt < 15) {
7182
- attempt++;
7183
- tryListen(port + 1);
7184
- } else {
7185
- reject(e);
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
- });
7200
- };
7201
- tryListen(preferredPort);
7202
- });
6375
+ async function ensureServer(timeoutMs = 12e3) {
6376
+ const existing = liveServer();
6377
+ if (existing) return { url: existing.url };
6378
+ try {
6379
+ const [bin, args, cwd] = BUNDLED ? [process.execPath, ["--experimental-sqlite", path3.join(__dirname, "web.mjs")], __dirname] : [process.execPath, ["--no-warnings", "--experimental-sqlite", "--import", "tsx", "apps/web/src/index.ts"], REPO_ROOT];
6380
+ const child = spawn(bin, args, {
6381
+ cwd,
6382
+ detached: true,
6383
+ stdio: "ignore",
6384
+ env: { ...process.env, CAREERMATE_NO_OPEN: "1" }
6385
+ });
6386
+ child.unref();
6387
+ } catch {
6388
+ return null;
6389
+ }
6390
+ const start = Date.now();
6391
+ while (Date.now() - start < timeoutMs) {
6392
+ const info2 = liveServer();
6393
+ if (info2) return { url: info2.url };
6394
+ await new Promise((r) => setTimeout(r, 300));
6395
+ }
6396
+ return null;
7203
6397
  }
7204
-
7205
- // apps/web/src/index.ts
7206
- var DEFAULT_PORT = Number(process.env.CAREERMATE_PORT ?? 4319);
7207
- function openBrowser(url) {
7208
- if (process.env.CAREERMATE_NO_OPEN === "1") return;
6398
+ function openInBrowser(url) {
7209
6399
  const platform = process.platform;
7210
6400
  try {
7211
6401
  if (platform === "win32") {
@@ -7218,23 +6408,505 @@ function openBrowser(url) {
7218
6408
  } catch {
7219
6409
  }
7220
6410
  }
7221
- async function main() {
7222
- const { url } = await startServer(DEFAULT_PORT);
7223
- const line = "\u2500".repeat(54);
7224
- console.log(`
7225
- \u250C${line}\u2510`);
7226
- console.log(` CareerMate v${APP_VERSION} \u2014 \uB0B4 \uCEE4\uB9AC\uC5B4 \uD750\uB984 \uAD00\uB9AC \uB3C4\uAD6C`);
7227
- console.log(`${" ".repeat(2)}${"\u2504".repeat(52)}`);
7228
- console.log(` \uB300\uC2DC\uBCF4\uB4DC ${url}`);
7229
- console.log(` \uC124\uCE58 \uC548\uB0B4 ${url}/install`);
7230
- console.log(` \uB370\uC774\uD130 \uC704\uCE58 ${getDataDir()}`);
7231
- console.log(` (\uBAA8\uB4E0 \uB370\uC774\uD130\uB294 \uC774 \uCEF4\uD4E8\uD130\uC5D0\uB9CC \uC800\uC7A5\uB429\uB2C8\uB2E4)`);
7232
- console.log(`\u2514${line}\u2518
7233
- `);
7234
- console.log(`\uC885\uB8CC\uD558\uB824\uBA74 Ctrl+C \uB97C \uB204\uB974\uC138\uC694.`);
7235
- openBrowser(url);
6411
+ async function resolveDashboardUrl(hash = "") {
6412
+ const live = liveServer();
6413
+ if (live) return { url: live.url + hash, running: true };
6414
+ const started = await ensureServer();
6415
+ if (started) return { url: started.url + hash, running: true };
6416
+ return { url: "http://127.0.0.1:4319" + hash, running: false };
7236
6417
  }
7237
- main().catch((err) => {
7238
- console.error("CareerMate \uC2DC\uC791 \uC2E4\uD328:", err instanceof Error ? err.message : err);
7239
- process.exit(1);
7240
- });
6418
+
6419
+ // packages/mcp-tools/src/tools.ts
6420
+ var STATUS_LIST = APPLICATION_STATUSES.map((s) => `${s}(${APPLICATION_STATUS_LABELS[s]})`).join(", ");
6421
+ var TOOLS = [
6422
+ /* ------------------------------------------------------------ onboarding */
6423
+ {
6424
+ name: "get_onboarding_status",
6425
+ title: "\uC628\uBCF4\uB529 \uC0C1\uD0DC \uD655\uC778",
6426
+ description: "CareerMate\uC5D0 \uC5B4\uB5A4 \uC815\uBCF4\uAC00 \uC900\uBE44\uB418\uC5B4 \uC788\uB294\uC9C0(\uD504\uB85C\uD544\xB7\uC774\uB825\uC11C\xB7\uACBD\uB825\xB7\uC2A4\uD0AC\xB7\uC790\uC18C\uC11C\xB7\uACF5\uACE0) \uD655\uC778\uD558\uACE0, \uC0AC\uC6A9\uC790\uAC00 \uC9C0\uAE08 \uD574\uC57C \uD560 \uB2E4\uC74C \uB2E8\uACC4\uB97C \uB3CC\uB824\uC90D\uB2C8\uB2E4. AI \uD074\uB77C\uC774\uC5B8\uD2B8\uAC00 \uC5F0\uACB0\uB41C \uC9C1\uD6C4 \uAC00\uC7A5 \uBA3C\uC800 \uD638\uCD9C\uD558\uC138\uC694. \uD504\uB85C\uD544 \uC644\uC131\uB3C4(0~100)\uB3C4 \uD3EC\uD568\uB429\uB2C8\uB2E4.",
6427
+ inputSchema: {},
6428
+ readOnly: true,
6429
+ handler: () => {
6430
+ const s = getOnboardingStatus();
6431
+ const lines = s.next_steps.map((t, i) => `${i + 1}. ${t}`).join("\n");
6432
+ return ok(
6433
+ `\uC628\uBCF4\uB529 ${s.completed ? "\uC644\uB8CC\uB428" : "\uC9C4\uD589 \uD544\uC694"} \xB7 \uD504\uB85C\uD544 \uC644\uC131\uB3C4 ${s.profile_completeness}%
6434
+
6435
+ \uB2E4\uC74C \uB2E8\uACC4:
6436
+ ${lines}`,
6437
+ s
6438
+ );
6439
+ }
6440
+ },
6441
+ {
6442
+ name: "start_onboarding",
6443
+ title: "\uC628\uBCF4\uB529 \uC2DC\uC791 \uC548\uB0B4",
6444
+ description: '\uAC00\uC774\uB4DC\uD615 \uC628\uBCF4\uB529\uC744 \uC2DC\uC791\uD569\uB2C8\uB2E4. \uD604\uC7AC \uC0C1\uD0DC\uC640 \uD568\uAED8, AI\uAC00 \uC0AC\uC6A9\uC790\uB97C \uC5B4\uB5BB\uAC8C \uC548\uB0B4\uD558\uBA74 \uB418\uB294\uC9C0(\uD504\uB85C\uD544 \uC785\uB825 \u2192 \uC774\uB825\uC11C/\uC790\uC18C\uC11C \uCD94\uAC00 \u2192 \uB300\uC2DC\uBCF4\uB4DC \uC5F4\uAE30)\uC5D0 \uB300\uD55C \uB2E8\uACC4\uBCC4 \uC9C0\uCE68\uC744 \uB3CC\uB824\uC90D\uB2C8\uB2E4. \uC0AC\uC6A9\uC790\uAC00 "\uC2DC\uC791\uD558\uC790/\uC124\uC815 \uB3C4\uC640\uC918"\uB77C\uACE0 \uD560 \uB54C \uC0AC\uC6A9\uD558\uC138\uC694.',
6445
+ inputSchema: {},
6446
+ readOnly: true,
6447
+ handler: () => {
6448
+ const s = getOnboardingStatus();
6449
+ return ok(
6450
+ `\uC628\uBCF4\uB529\uC744 \uC2DC\uC791\uD569\uB2C8\uB2E4. \uC544\uB798 \uC9C0\uCE68\uC744 \uB530\uB77C \uC0AC\uC6A9\uC790\uB97C \uC548\uB0B4\uD558\uC138\uC694.
6451
+
6452
+ ${ONBOARDING_PROMPT}`,
6453
+ { status: s, guide: ONBOARDING_PROMPT }
6454
+ );
6455
+ }
6456
+ },
6457
+ /* --------------------------------------------------------------- profile */
6458
+ {
6459
+ name: "get_profile",
6460
+ title: "\uD504\uB85C\uD544 \uC870\uD68C",
6461
+ description: "\uC0AC\uC6A9\uC790\uC758 \uAE30\uBCF8 \uD504\uB85C\uD544(\uC774\uB984, \uD55C \uC904 \uC18C\uAC1C, \uC694\uC57D, \uD76C\uB9DD \uC9C1\uBB34/\uC870\uAC74, \uC120\uD638 \uBB38\uCCB4, \uAC15\uC870 \uD3EC\uC778\uD2B8, \uB9C1\uD06C)\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4.",
6462
+ inputSchema: {},
6463
+ readOnly: true,
6464
+ handler: () => {
6465
+ const p = profileRepo.get();
6466
+ return p ? ok(`\uD504\uB85C\uD544: ${p.name ?? "(\uC774\uB984 \uC5C6\uC74C)"}`, p) : ok("\uC544\uC9C1 \uC800\uC7A5\uB41C \uD504\uB85C\uD544\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. save_profile\uB85C \uBA3C\uC800 \uC800\uC7A5\uD558\uC138\uC694.", null);
6467
+ }
6468
+ },
6469
+ {
6470
+ name: "save_profile",
6471
+ title: "\uD504\uB85C\uD544 \uC800\uC7A5",
6472
+ description: "\uC0AC\uC6A9\uC790 \uD504\uB85C\uD544\uC744 \uC800\uC7A5\uD569\uB2C8\uB2E4. \uC804\uB2EC\uD55C \uD544\uB4DC\uB9CC \uAC31\uC2E0\uB418\uACE0 \uB098\uBA38\uC9C0\uB294 \uC720\uC9C0\uB429\uB2C8\uB2E4(\uBD80\uBD84 \uC800\uC7A5 \uC548\uC804). \uC774\uB825\uC11C\uB97C \uAD6C\uC870\uD654\uD55C \uB4A4 \uC774\uB984/\uD55C \uC904 \uC18C\uAC1C/\uD76C\uB9DD \uC9C1\uBB34/\uC120\uD638 \uBB38\uCCB4/\uAC15\uC870 \uD3EC\uC778\uD2B8 \uB4F1\uC744 \uCC44\uC6CC \uC800\uC7A5\uD558\uC138\uC694. \uC120\uD638 \uBB38\uCCB4(preferred_tone)\uC640 \uAC15\uC870 \uD3EC\uC778\uD2B8(emphasis_points)\uB294 \uC774\uD6C4 \uC790\uAE30\uC18C\uAC1C\uC11C \uC791\uC131 \uD488\uC9C8\uC5D0 \uC9C1\uC811 \uC601\uD5A5\uC744 \uC90D\uB2C8\uB2E4.",
6473
+ inputSchema: ProfileInputSchema.shape,
6474
+ handler: (args) => {
6475
+ const p = saveProfile(args);
6476
+ return ok(`\uD504\uB85C\uD544\uC744 \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4: ${p.name ?? ""}`.trim(), p);
6477
+ }
6478
+ },
6479
+ {
6480
+ name: "update_profile",
6481
+ title: "\uD504\uB85C\uD544 \uBD80\uBD84 \uC218\uC815",
6482
+ description: "\uAE30\uC874 \uD504\uB85C\uD544\uC744 \uC720\uC9C0\uD558\uBA74\uC11C \uC77C\uBD80 \uD544\uB4DC\uB9CC \uC218\uC815\uD569\uB2C8\uB2E4. save_profile\uACFC \uB3D9\uC77C\uD558\uAC8C \uB3D9\uC791\uD558\uBA70, \uD55C\uB450 \uAC1C \uD56D\uBAA9\uB9CC \uBC14\uAFC0 \uB54C \uC758\uBBF8\uB97C \uBD84\uBA85\uD788 \uD558\uAE30 \uC704\uD574 \uC0AC\uC6A9\uD569\uB2C8\uB2E4.",
6483
+ inputSchema: ProfileInputSchema.partial().shape,
6484
+ handler: (args) => {
6485
+ const p = saveProfile(args);
6486
+ return ok("\uD504\uB85C\uD544\uC744 \uC218\uC815\uD588\uC2B5\uB2C8\uB2E4.", p);
6487
+ }
6488
+ },
6489
+ /* --------------------------------------------------------------- resumes */
6490
+ {
6491
+ name: "add_resume",
6492
+ title: "\uC774\uB825\uC11C/\uACBD\uB825\uAE30\uC220\uC11C \uCD94\uAC00",
6493
+ description: "\uC774\uB825\uC11C, \uACBD\uB825\uAE30\uC220\uC11C, \uD3EC\uD2B8\uD3F4\uB9AC\uC624 \uB4F1 \uBB38\uC11C\uB97C \uC800\uC7A5\uD569\uB2C8\uB2E4. content\uC5D0\uB294 \uC0AC\uC6A9\uC790\uAC00 \uC5C5\uB85C\uB4DC/\uBD99\uC5EC\uB123\uC740 \uD14D\uC2A4\uD2B8(\uB610\uB294 \uC815\uB9AC\uD55C Markdown)\uB97C \uB123\uC73C\uC138\uC694. kind \uAE30\uBCF8\uAC12\uC740 resume\uC785\uB2C8\uB2E4. is_primary=true\uB85C \uB300\uD45C \uBB38\uC11C\uB97C \uC9C0\uC815\uD558\uBA74 get_application_context\uC758 primary_resume\uB85C \uB178\uCD9C\uB429\uB2C8\uB2E4.",
6494
+ inputSchema: {
6495
+ title: external_exports.string().describe("\uBB38\uC11C \uC81C\uBAA9"),
6496
+ content: external_exports.string().describe("\uBCF8\uBB38 (Markdown/\uC77C\uBC18 \uD14D\uC2A4\uD2B8)"),
6497
+ kind: external_exports.enum(DOCUMENT_KINDS).optional().describe("resume(\uC774\uB825\uC11C)/career_description(\uACBD\uB825\uAE30\uC220\uC11C)/portfolio/other"),
6498
+ source: external_exports.enum(CONTENT_SOURCES).optional(),
6499
+ is_primary: external_exports.boolean().optional().describe("\uB300\uD45C \uBB38\uC11C \uC5EC\uBD80"),
6500
+ tags: external_exports.array(external_exports.string()).optional()
6501
+ },
6502
+ handler: (args) => {
6503
+ const doc = addResume(args);
6504
+ return ok(`'${doc.title}' \uBB38\uC11C\uB97C \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4.`, doc);
6505
+ }
6506
+ },
6507
+ {
6508
+ name: "get_resumes",
6509
+ title: "\uC774\uB825\uC11C \uBAA9\uB85D \uC870\uD68C",
6510
+ description: "\uC800\uC7A5\uB41C \uC774\uB825\uC11C/\uACBD\uB825\uAE30\uC220\uC11C \uB4F1 \uBB38\uC11C\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4. kind\uB85C \uC885\uB958\uB97C \uD544\uD130\uB9C1\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.",
6511
+ inputSchema: { kind: external_exports.enum(DOCUMENT_KINDS).optional() },
6512
+ readOnly: true,
6513
+ handler: (args) => {
6514
+ const docs = documentRepo.list(args?.kind);
6515
+ return ok(`\uBB38\uC11C ${docs.length}\uAC74`, docs);
6516
+ }
6517
+ },
6518
+ /* ----------------------------------------------------------- experiences */
6519
+ {
6520
+ name: "add_experience",
6521
+ title: "\uACBD\uB825 \uCD94\uAC00",
6522
+ description: "\uAD6C\uC870\uD654\uB41C \uACBD\uB825(\uC9C1\uC7A5 \uB2E8\uC704)\uC744 \uBCC4\uB3C4 \uD56D\uBAA9\uC73C\uB85C \uC800\uC7A5\uD569\uB2C8\uB2E4. \uC774\uB825\uC11C \uBCF8\uBB38(add_resume)\uACFC \uB2EC\uB9AC, \uD68C\uC0AC\xB7\uC9C1\uBB34\xB7\uAE30\uAC04\xB7\uC131\uACFC\uB97C \uD544\uB4DC\uB85C \uB098\uB220 \uC800\uC7A5\uD574 \uC801\uD569\uB3C4 \uBD84\uC11D/\uC790\uC18C\uC11C \uC791\uC131 \uC2DC \uC815\uBC00\uD558\uAC8C \uD65C\uC6A9\uB429\uB2C8\uB2E4. company\uB294 \uD544\uC218\uC774\uBA70, achievements\uC5D0\uB294 \uC815\uB7C9 \uC9C0\uD45C\uAC00 \uB2F4\uAE34 \uC131\uACFC\uB97C, tech\uC5D0\uB294 \uC0AC\uC6A9 \uAE30\uC220\uC744 \uBC30\uC5F4\uB85C \uB123\uC73C\uC138\uC694. \uC628\uBCF4\uB529 \uC644\uC131\uB3C4(get_onboarding_status)\uC758 has_experience\uB97C \uCC44\uC6B0\uB824\uBA74 \uC774 \uB3C4\uAD6C\uB85C 1\uAC74 \uC774\uC0C1 \uCD94\uAC00\uD574\uC57C \uD569\uB2C8\uB2E4.",
6523
+ inputSchema: ExperienceInputSchema.shape,
6524
+ handler: (args) => {
6525
+ const exp = addExperience(args);
6526
+ return ok(`\uACBD\uB825 '${exp.company}${exp.role ? ` \xB7 ${exp.role}` : ""}'\uC744(\uB97C) \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, exp);
6527
+ }
6528
+ },
6529
+ {
6530
+ name: "get_experiences",
6531
+ title: "\uACBD\uB825 \uBAA9\uB85D \uC870\uD68C",
6532
+ description: "\uC800\uC7A5\uB41C \uACBD\uB825\uC744 \uC2DC\uAC04 \uC5ED\uC21C\uC73C\uB85C \uC870\uD68C\uD569\uB2C8\uB2E4. (\uC804\uCCB4 \uB9E5\uB77D\uC774 \uD544\uC694\uD558\uBA74 get_application_context\uB97C \uC4F0\uC138\uC694.)",
6533
+ inputSchema: {},
6534
+ readOnly: true,
6535
+ handler: () => {
6536
+ const list = experienceRepo.list();
6537
+ return ok(`\uACBD\uB825 ${list.length}\uAC74`, list);
6538
+ }
6539
+ },
6540
+ /* -------------------------------------------------------------- projects */
6541
+ {
6542
+ name: "add_project",
6543
+ title: "\uD504\uB85C\uC81D\uD2B8 \uCD94\uAC00",
6544
+ description: "\uB300\uD45C \uD504\uB85C\uC81D\uD2B8\uB97C \uBCC4\uB3C4 \uD56D\uBAA9\uC73C\uB85C \uC800\uC7A5\uD569\uB2C8\uB2E4. name\uC740 \uD544\uC218\uC774\uBA70, highlights(\uD575\uC2EC \uC131\uACFC)\uC640 tech(\uC0AC\uC6A9 \uAE30\uC220)\uB97C \uBC30\uC5F4\uB85C \uB123\uC73C\uBA74 \uC790\uC18C\uC11C\xB7\uBA74\uC811 \uC900\uBE44\uC5D0\uC11C \uADFC\uAC70\uB85C \uD65C\uC6A9\uB429\uB2C8\uB2E4. url\uC5D0 \uACB0\uACFC\uBB3C \uB9C1\uD06C\uB97C \uB123\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4.",
6545
+ inputSchema: ProjectInputSchema.shape,
6546
+ handler: (args) => {
6547
+ const prj = addProject(args);
6548
+ return ok(`\uD504\uB85C\uC81D\uD2B8 '${prj.name}'\uC744(\uB97C) \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, prj);
6549
+ }
6550
+ },
6551
+ {
6552
+ name: "get_projects",
6553
+ title: "\uD504\uB85C\uC81D\uD2B8 \uBAA9\uB85D \uC870\uD68C",
6554
+ description: "\uC800\uC7A5\uB41C \uD504\uB85C\uC81D\uD2B8\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4. (\uC804\uCCB4 \uB9E5\uB77D\uC774 \uD544\uC694\uD558\uBA74 get_application_context\uB97C \uC4F0\uC138\uC694.)",
6555
+ inputSchema: {},
6556
+ readOnly: true,
6557
+ handler: () => {
6558
+ const list = projectRepo.list();
6559
+ return ok(`\uD504\uB85C\uC81D\uD2B8 ${list.length}\uAC74`, list);
6560
+ }
6561
+ },
6562
+ /* ---------------------------------------------------------------- skills */
6563
+ {
6564
+ name: "add_skill",
6565
+ title: "\uAE30\uC220\uC2A4\uD0DD \uCD94\uAC00",
6566
+ description: "\uBCF4\uC720 \uAE30\uC220\uC2A4\uD0DD\uC744 \uBCC4\uB3C4 \uD56D\uBAA9\uC73C\uB85C \uC800\uC7A5\uD569\uB2C8\uB2E4. name\uC740 \uD544\uC218\uC774\uBA70, category(\uC5B8\uC5B4/\uD504\uB808\uC784\uC6CC\uD06C/\uD234/\uC18C\uD504\uD2B8\uC2A4\uD0AC \uB4F1)\xB7level\xB7years\uB85C \uC219\uB828\uB3C4\uB97C \uD568\uAED8 \uAE30\uB85D\uD558\uBA74 \uACF5\uACE0 \uD0A4\uC6CC\uB4DC \uB9E4\uCE6D\uC774 \uC815\uD655\uD574\uC9D1\uB2C8\uB2E4. \uC628\uBCF4\uB529 \uC644\uC131\uB3C4\uC758 has_skills\uB97C \uCC44\uC6B0\uB824\uBA74 \uC774 \uB3C4\uAD6C\uB85C 1\uAC74 \uC774\uC0C1 \uCD94\uAC00\uD574\uC57C \uD569\uB2C8\uB2E4. \uC5EC\uB7EC \uAC1C\uBA74 \uB3C4\uAD6C\uB97C \uBC18\uBCF5 \uD638\uCD9C\uD558\uC138\uC694.",
6567
+ inputSchema: SkillInputSchema.shape,
6568
+ handler: (args) => {
6569
+ const skill = addSkill(args);
6570
+ return ok(`\uAE30\uC220 '${skill.name}'\uC744(\uB97C) \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, skill);
6571
+ }
6572
+ },
6573
+ {
6574
+ name: "get_skills",
6575
+ title: "\uAE30\uC220\uC2A4\uD0DD \uBAA9\uB85D \uC870\uD68C",
6576
+ description: "\uC800\uC7A5\uB41C \uAE30\uC220\uC2A4\uD0DD\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. (\uC804\uCCB4 \uB9E5\uB77D\uC774 \uD544\uC694\uD558\uBA74 get_application_context\uB97C \uC4F0\uC138\uC694.)",
6577
+ inputSchema: {},
6578
+ readOnly: true,
6579
+ handler: () => {
6580
+ const list = skillRepo.list();
6581
+ return ok(`\uAE30\uC220 ${list.length}\uAC74`, list);
6582
+ }
6583
+ },
6584
+ /* ---------------------------------------------------------- cover letters */
6585
+ {
6586
+ name: "add_cover_letter",
6587
+ title: "\uC790\uAE30\uC18C\uAC1C\uC11C \uCD94\uAC00",
6588
+ description: '\uAE30\uC874 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uB4F1\uB85D\uD569\uB2C8\uB2E4. content\uB97C \uD568\uAED8 \uC8FC\uBA74 \uCCAB \uBC88\uC9F8 \uBC84\uC804(v1)\uC73C\uB85C \uC800\uC7A5\uB429\uB2C8\uB2E4. \uC0AC\uC6A9\uC790\uAC00 \uC774\uBBF8 \uC4F4 \uC790\uC18C\uC11C\uB97C \uD559\uC2B5\uC6A9\uC73C\uB85C \uBCF4\uAD00\uD560 \uB54C \uC0AC\uC6A9\uD558\uC138\uC694. \uC0C8 \uACF5\uACE0\uC6A9 \uC790\uC18C\uC11C\uB97C "\uC0DD\uC131"\uD574\uC11C \uC800\uC7A5\uD560 \uB54C\uB294 save_cover_letter_version\uC744 \uC0AC\uC6A9\uD558\uB294 \uD3B8\uC774 \uBC84\uC804 \uAD00\uB9AC\uC5D0 \uC88B\uC2B5\uB2C8\uB2E4.',
6589
+ inputSchema: {
6590
+ title: external_exports.string(),
6591
+ content: external_exports.string().optional(),
6592
+ job_id: external_exports.string().optional(),
6593
+ is_primary: external_exports.boolean().optional(),
6594
+ note: external_exports.string().optional(),
6595
+ source: external_exports.enum(CONTENT_SOURCES).optional()
6596
+ },
6597
+ handler: (args) => {
6598
+ if (args.content) {
6599
+ const { coverLetter, version } = saveCoverLetterVersion({
6600
+ title: args.title,
6601
+ job_id: args.job_id,
6602
+ content: args.content,
6603
+ note: args.note,
6604
+ source: args.source ?? "manual"
6605
+ });
6606
+ if (args.is_primary) coverLetterRepo.setPrimary(coverLetter.id);
6607
+ return ok(`'${coverLetter.title}' \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4 (v${version.version_no}).`, coverLetter);
6608
+ }
6609
+ const cl = coverLetterRepo.create({ title: args.title, job_id: args.job_id ?? null, is_primary: args.is_primary });
6610
+ return ok(`'${cl.title}' \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC0DD\uC131\uD588\uC2B5\uB2C8\uB2E4.`, cl);
6611
+ }
6612
+ },
6613
+ {
6614
+ name: "get_cover_letters",
6615
+ title: "\uC790\uAE30\uC18C\uAC1C\uC11C \uBAA9\uB85D \uC870\uD68C",
6616
+ description: "\uC800\uC7A5\uB41C \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4. job_id\uB97C \uC8FC\uBA74 \uD574\uB2F9 \uACF5\uACE0\uC5D0 \uC5F0\uACB0\uB41C \uC790\uC18C\uC11C\uB9CC, \uC5C6\uC73C\uBA74 \uC804\uCCB4\uB97C \uB3CC\uB824\uC90D\uB2C8\uB2E4. \uAC01 \uD56D\uBAA9\uC740 \uD604\uC7AC \uBC84\uC804 \uB0B4\uC6A9\uACFC \uBC84\uC804 \uC218\uB97C \uD3EC\uD568\uD569\uB2C8\uB2E4.",
6617
+ inputSchema: { job_id: external_exports.string().optional() },
6618
+ readOnly: true,
6619
+ handler: (args) => {
6620
+ const list = args?.job_id ? coverLetterRepo.listByJob(args.job_id) : coverLetterRepo.list();
6621
+ return ok(`\uC790\uAE30\uC18C\uAC1C\uC11C ${list.length}\uAC74`, list);
6622
+ }
6623
+ },
6624
+ {
6625
+ name: "save_cover_letter_version",
6626
+ title: "\uC790\uAE30\uC18C\uAC1C\uC11C \uBC84\uC804 \uC800\uC7A5",
6627
+ description: '\uC790\uAE30\uC18C\uAC1C\uC11C\uC758 \uC0C8 \uBC84\uC804\uC744 \uC800\uC7A5\uD569\uB2C8\uB2E4. CareerMate \uC790\uC18C\uC11C \uC791\uC131 \uC6CC\uD06C\uD50C\uB85C\uC6B0\uC758 \uD575\uC2EC \uC800\uC7A5 \uB2E8\uACC4\uC785\uB2C8\uB2E4. cover_letter_id\uB97C \uC8FC\uBA74 \uAE30\uC874 \uC790\uC18C\uC11C\uC5D0 \uC0C8 \uBC84\uC804\uC744 \uCD94\uAC00\uD558\uACE0, \uC5C6\uC73C\uBA74 \uC0C8 \uC790\uC18C\uC11C\uB97C \uB9CC\uB4E4\uC5B4 v1\uB85C \uC800\uC7A5\uD569\uB2C8\uB2E4(\uC774\uB54C title \uAD8C\uC7A5). note\uC5D0 \uC774 \uBC84\uC804\uC758 \uBCC0\uACBD \uC694\uC57D(\uC608: "\uC9C0\uC6D0\uB3D9\uAE30 \uBCF4\uAC15")\uC744 \uB0A8\uAE30\uBA74 \uC0AC\uC6A9\uC790\uAC00 \uB300\uC2DC\uBCF4\uB4DC\uC5D0\uC11C \uBC84\uC804 \uD788\uC2A4\uD1A0\uB9AC\uB97C \uC774\uD574\uD558\uAE30 \uC27D\uC2B5\uB2C8\uB2E4. job_id\uB85C \uACF5\uACE0\uC5D0 \uC5F0\uACB0\uD558\uBA74 \uC9C0\uC6D0 \uD56D\uBAA9\uACFC \uC790\uB3D9 \uC5F0\uACB0\uB429\uB2C8\uB2E4.',
6628
+ inputSchema: CoverLetterVersionInputSchema.shape,
6629
+ handler: (args) => {
6630
+ const { coverLetter, version } = saveCoverLetterVersion(args);
6631
+ return ok(`'${coverLetter.title}' v${version.version_no}\uC744 \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4.`, { coverLetter, version });
6632
+ }
6633
+ },
6634
+ /* ------------------------------------------------------------------ jobs */
6635
+ {
6636
+ name: "save_job_posting",
6637
+ title: "\uCC44\uC6A9\uACF5\uACE0 \uC800\uC7A5",
6638
+ description: "\uCC44\uC6A9\uACF5\uACE0\uB97C \uC800\uC7A5\uD569\uB2C8\uB2E4(\uC788\uC73C\uBA74 \uAC31\uC2E0). company\uC640 position\uC740 \uD544\uC218\uC785\uB2C8\uB2E4. \uACF5\uACE0 \uC6D0\uBB38\uC740 description\uC5D0, \uD575\uC2EC \uC790\uACA9\uC694\uAC74/\uC6B0\uB300\uC0AC\uD56D\uC740 requirements \uBC30\uC5F4\uC5D0, \uD575\uC2EC \uD0A4\uC6CC\uB4DC\uB294 keywords\uC5D0 \uC815\uB9AC\uD574 \uB123\uC73C\uBA74 \uC774\uD6C4 \uC801\uD569\uB3C4 \uBD84\uC11D/\uC790\uC18C\uC11C \uC791\uC131\uC5D0 \uD65C\uC6A9\uB429\uB2C8\uB2E4. \uAC19\uC740 url\uC774\uBA74 \uC911\uBCF5 \uC0DD\uC131 \uC5C6\uC774 \uAC31\uC2E0\uB429\uB2C8\uB2E4. \uC800\uC7A5 \uC2DC \uC9C0\uC6D0(application) \uD56D\uBAA9\uC774 \uC790\uB3D9\uC73C\uB85C \uC0DD\uC131\uB429\uB2C8\uB2E4.",
6639
+ inputSchema: JobInputSchema.shape,
6640
+ handler: (args) => {
6641
+ const { job } = saveJobPosting(args);
6642
+ return ok(`'${job.company} \xB7 ${job.position}' \uACF5\uACE0\uB97C \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4. (job_id: ${job.id})`, job);
6643
+ }
6644
+ },
6645
+ {
6646
+ name: "get_job_posting",
6647
+ title: "\uCC44\uC6A9\uACF5\uACE0 \uC0C1\uC138 \uC870\uD68C",
6648
+ description: "\uACF5\uACE0 1\uAC74\uC758 \uC0C1\uC138\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4. \uACF5\uACE0 \uC815\uBCF4\uC640 \uD568\uAED8 \uC801\uD569\uB3C4 \uBD84\uC11D, \uC9C0\uC6D0 \uC0C1\uD0DC, \uC5F0\uACB0\uB41C \uC790\uAE30\uC18C\uAC1C\uC11C, \uBA74\uC811 \uC900\uBE44 \uC790\uB8CC\uB97C \uD55C \uBC88\uC5D0 \uB3CC\uB824\uC90D\uB2C8\uB2E4.",
6649
+ inputSchema: { job_id: external_exports.string() },
6650
+ readOnly: true,
6651
+ handler: (args) => {
6652
+ const job = jobRepo.get(args.job_id);
6653
+ if (!job) return fail(`\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${args.job_id}`);
6654
+ return ok(`${job.company} \xB7 ${job.position}`, {
6655
+ job: jobWithMeta(job),
6656
+ fit: fitRepo.getByJob(job.id),
6657
+ application: applicationRepo.getByJob(job.id),
6658
+ cover_letters: coverLetterRepo.listByJob(job.id),
6659
+ interview: interviewRepo.getByJob(job.id)
6660
+ });
6661
+ }
6662
+ },
6663
+ {
6664
+ name: "list_jobs",
6665
+ title: "\uC800\uC7A5\uB41C \uACF5\uACE0 \uBAA9\uB85D",
6666
+ description: "\uC800\uC7A5\uB41C \uBAA8\uB4E0 \uCC44\uC6A9\uACF5\uACE0\uB97C \uC9C0\uC6D0 \uC0C1\uD0DC\uC640 \uC801\uD569\uB3C4 \uC810\uC218\uC640 \uD568\uAED8 \uBAA9\uB85D\uC73C\uB85C \uB3CC\uB824\uC90D\uB2C8\uB2E4.",
6667
+ inputSchema: {},
6668
+ readOnly: true,
6669
+ handler: () => {
6670
+ const jobs = jobRepo.list().map(jobWithMeta);
6671
+ return ok(`\uACF5\uACE0 ${jobs.length}\uAC74`, jobs);
6672
+ }
6673
+ },
6674
+ {
6675
+ name: "parse_job_posting",
6676
+ title: "\uACF5\uACE0 \uD14D\uC2A4\uD2B8 \uC815\uB9AC",
6677
+ description: "\uBD99\uC5EC\uB123\uC740 \uACF5\uACE0 \uC6D0\uBB38(\uB610\uB294 \uD398\uC774\uC9C0 \uD14D\uC2A4\uD2B8)\uC744 \uC815\uB9AC\uD574 \uD68C\uC0AC\uBA85/\uC9C1\uBB34/\uB9C8\uAC10\uC77C \uCD94\uC815\uACFC \uAE30\uC220 \uD0A4\uC6CC\uB4DC\uB97C \uCD94\uCD9C\uD569\uB2C8\uB2E4. save_job_posting \uC804\uC5D0 \uBE60\uB974\uAC8C \uAD6C\uC870\uD654\uD560 \uB54C \uBCF4\uC870\uC801\uC73C\uB85C \uC0AC\uC6A9\uD558\uC138\uC694. (\uBD84\uC11D/\uD310\uB2E8\uC740 AI\uAC00 \uC9C1\uC811 \uD558\uACE0, \uC774 \uB3C4\uAD6C\uB294 \uD14D\uC2A4\uD2B8 \uC815\uB9AC\uB9CC \uD569\uB2C8\uB2E4.)",
6678
+ inputSchema: { raw: external_exports.string().describe("\uACF5\uACE0 \uC6D0\uBB38 \uD14D\uC2A4\uD2B8") },
6679
+ readOnly: true,
6680
+ handler: (args) => {
6681
+ const cleaned = cleanJobPosting(args.raw ?? "");
6682
+ return ok("\uACF5\uACE0 \uD14D\uC2A4\uD2B8\uB97C \uC815\uB9AC\uD588\uC2B5\uB2C8\uB2E4.", cleaned);
6683
+ }
6684
+ },
6685
+ /* ----------------------------------------------------- application context */
6686
+ {
6687
+ name: "get_application_context",
6688
+ title: "\uC9C0\uC6D0 \uB9E5\uB77D \uD55C \uBC88\uC5D0 \uAC00\uC838\uC624\uAE30 \u2B50",
6689
+ description: "\u2605 \uAC00\uC7A5 \uC911\uC694\uD55C \uB3C4\uAD6C. \uACF5\uACE0\uB97C \uBD84\uC11D\uD558\uAC70\uB098 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC791\uC131\uD558\uAE30 \uC804\uC5D0 \uBC18\uB4DC\uC2DC \uBA3C\uC800 \uD638\uCD9C\uD558\uC138\uC694. \uD55C \uBC88\uC758 \uD638\uCD9C\uB85C \uB2E4\uC74C\uC744 \uBAA8\uB450 \uB3CC\uB824\uC90D\uB2C8\uB2E4: \uC0AC\uC6A9\uC790 \uD504\uB85C\uD544, \uB300\uD45C \uC774\uB825\uC11C, \uC804\uCCB4 \uACBD\uB825, \uD504\uB85C\uC81D\uD2B8, \uAE30\uC220\uC2A4\uD0DD, \uAE30\uC874 \uC790\uAE30\uC18C\uAC1C\uC11C, \uCD5C\uADFC \uC9C0\uC6D0 \uC774\uB825, (job_id\uB97C \uC8FC\uBA74) \uB300\uC0C1 \uACF5\uACE0\uC640 \uC774\uC804 \uC801\uD569\uB3C4 \uBD84\uC11D, \uAC19\uC740 \uD68C\uC0AC/\uC9C1\uBB34 \uAD00\uB828 \uC774\uC804 \uAE30\uB85D, \uADF8\uB9AC\uACE0 \uC0AC\uC6A9\uC790\uC758 \uC120\uD638 \uBB38\uCCB4\xB7\uAC15\uC870 \uD3EC\uC778\uD2B8. \uC774 \uB370\uC774\uD130\uB97C \uADFC\uAC70\uB85C \uBD84\uC11D/\uC791\uC131\uD558\uACE0, \uACB0\uACFC\uB294 save_fit_analysis / save_cover_letter_version\uC73C\uB85C \uB2E4\uC2DC \uC800\uC7A5\uD558\uC138\uC694. CareerMate\uB294 \uB370\uC774\uD130\uB97C \uC81C\uACF5\uD560 \uBFD0, \uBD84\uC11D/\uC791\uC131\uC740 \uB2F9\uC2E0(AI)\uC774 \uC218\uD589\uD569\uB2C8\uB2E4.",
6690
+ inputSchema: { job_id: external_exports.string().optional().describe("\uD2B9\uC815 \uACF5\uACE0 \uAE30\uC900\uC73C\uB85C \uB9E5\uB77D\uC744 \uBAA8\uC744 \uB54C") },
6691
+ readOnly: true,
6692
+ handler: (args) => {
6693
+ const ctx = getApplicationContext({ job_id: args?.job_id });
6694
+ const summary = [
6695
+ `\uD504\uB85C\uD544: ${ctx.profile?.name ?? "\uBBF8\uC785\uB825"}`,
6696
+ `\uC774\uB825\uC11C ${ctx.resumes.length} \xB7 \uACBD\uB825 ${ctx.experiences.length} \xB7 \uD504\uB85C\uC81D\uD2B8 ${ctx.projects.length} \xB7 \uC2A4\uD0AC ${ctx.skills.length} \xB7 \uC790\uC18C\uC11C ${ctx.cover_letters.length}`,
6697
+ ctx.job ? `\uB300\uC0C1 \uACF5\uACE0: ${ctx.job.company} \xB7 ${ctx.job.position}` : "\uB300\uC0C1 \uACF5\uACE0 \uC5C6\uC74C",
6698
+ ctx.writing_preferences.preferred_tone ? `\uC120\uD638 \uBB38\uCCB4: ${ctx.writing_preferences.preferred_tone}` : "",
6699
+ "\u{1F4A1} \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC4F8 \uB54C\uB294 get_writing_style_guide\uB85C AI \uD2F0 \uC548 \uB098\uB294 \uAE00\uC4F0\uAE30 \uADDC\uCE59\uC744 \uD568\uAED8 \uC801\uC6A9\uD558\uC138\uC694."
6700
+ ].filter(Boolean).join("\n");
6701
+ return ok(summary, ctx);
6702
+ }
6703
+ },
6704
+ {
6705
+ name: "save_fit_analysis",
6706
+ title: "\uC801\uD569\uB3C4 \uBD84\uC11D \uACB0\uACFC \uC800\uC7A5",
6707
+ description: "\uACF5\uACE0\uC640 \uC0AC\uC6A9\uC790 \uC815\uBCF4\uB97C \uBE44\uAD50\uD574 \uB2F9\uC2E0(AI)\uC774 \uB3C4\uCD9C\uD55C \uC801\uD569\uB3C4 \uBD84\uC11D\uC744 \uC800\uC7A5\uD569\uB2C8\uB2E4. job_id\uB294 \uD544\uC218\uC774\uBA70 \uBA3C\uC800 save_job_posting\uC73C\uB85C \uACF5\uACE0\uAC00 \uC800\uC7A5\uB418\uC5B4 \uC788\uC5B4\uC57C \uD569\uB2C8\uB2E4. score(0~100), summary, strengths(\uAC15\uC810), gaps(\uBCF4\uC644 \uD544\uC694), matched/missing_keywords, recommendations(\uC790\uC18C\uC11C\xB7\uC9C0\uC6D0 \uC804\uB7B5 \uC81C\uC548)\uB97C \uCC44\uC6CC \uC800\uC7A5\uD558\uC138\uC694. \uAC19\uC740 \uACF5\uACE0\uC5D0 \uB2E4\uC2DC \uC800\uC7A5\uD558\uBA74 \uAC31\uC2E0\uB429\uB2C8\uB2E4.",
6708
+ inputSchema: FitAnalysisInputSchema.shape,
6709
+ handler: (args) => {
6710
+ try {
6711
+ const fit = saveFitAnalysis(args);
6712
+ return ok(`\uC801\uD569\uB3C4 \uBD84\uC11D\uC744 \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4${fit.score != null ? ` (${fit.score}\uC810)` : ""}.`, fit);
6713
+ } catch (e) {
6714
+ return fail(e instanceof Error ? e.message : "\uC800\uC7A5 \uC2E4\uD328");
6715
+ }
6716
+ }
6717
+ },
6718
+ /* ------------------------------------------------- application status flow */
6719
+ {
6720
+ name: "update_application_status",
6721
+ title: "\uC9C0\uC6D0 \uC0C1\uD0DC \uBCC0\uACBD",
6722
+ description: `\uC9C0\uC6D0 \uC0C1\uD0DC\uB97C \uBCC0\uACBD\uD569\uB2C8\uB2E4. \uAC00\uB2A5\uD55C \uC0C1\uD0DC: ${STATUS_LIST}. '\uC11C\uB958 \uD569\uACA9(document_passed)' \uC774\uC0C1\uC73C\uB85C \uBC14\uB00C\uBA74 \uBA74\uC811 \uC900\uBE44\uB97C \uC81C\uC548\uD558\uB294 \uD78C\uD2B8\uB97C \uD568\uAED8 \uB3CC\uB824\uC90D\uB2C8\uB2E4. note\uC5D0 \uBCC0\uACBD \uC0AC\uC720\uB97C \uB0A8\uAE38 \uC218 \uC788\uC2B5\uB2C8\uB2E4.`,
6723
+ inputSchema: {
6724
+ job_id: external_exports.string(),
6725
+ status: external_exports.enum(APPLICATION_STATUSES),
6726
+ note: external_exports.string().optional()
6727
+ },
6728
+ handler: (args) => {
6729
+ try {
6730
+ const res = updateApplicationStatus(args.job_id, args.status, args.note);
6731
+ return ok(
6732
+ `\uC0C1\uD0DC\uB97C '${APPLICATION_STATUS_LABELS[args.status]}'(\uC73C)\uB85C \uBCC0\uACBD\uD588\uC2B5\uB2C8\uB2E4.${res.hint ? `
6733
+
6734
+ \u{1F4A1} ${res.hint}` : ""}`,
6735
+ res
6736
+ );
6737
+ } catch (e) {
6738
+ return fail(e instanceof Error ? e.message : "\uBCC0\uACBD \uC2E4\uD328");
6739
+ }
6740
+ }
6741
+ },
6742
+ /* ---------------------------------------------------------- interview prep */
6743
+ {
6744
+ name: "save_interview_prep",
6745
+ title: "\uBA74\uC811 \uC900\uBE44 \uC790\uB8CC \uC800\uC7A5",
6746
+ description: "\uC608\uC0C1 \uBA74\uC811 \uC9C8\uBB38, \uAF2C\uB9AC \uC9C8\uBB38, STAR \uB2F5\uBCC0 \uAC00\uC774\uB4DC, 1\uBD84 \uC790\uAE30\uC18C\uAC1C \uCD08\uC548 \uB4F1 \uBA74\uC811 \uC900\uBE44 \uC790\uB8CC\uB97C \uC800\uC7A5\uD569\uB2C8\uB2E4. \uBCF4\uD1B5 \uC9C0\uC6D0 \uC0C1\uD0DC\uAC00 \uC11C\uB958 \uD569\uACA9\uC73C\uB85C \uBC14\uB010 \uB4A4, \uD574\uB2F9 \uACF5\uACE0/\uC9C1\uBB34/\uC790\uC18C\uC11C\uB97C \uADFC\uAC70\uB85C \uB2F9\uC2E0(AI)\uC774 \uC0DD\uC131\uD55C \uC790\uB8CC\uB97C \uC800\uC7A5\uD558\uB294 \uB370 \uC0AC\uC6A9\uD569\uB2C8\uB2E4. job_id\uB294 \uD544\uC218\uC785\uB2C8\uB2E4. \uAC19\uC740 \uACF5\uACE0\uC5D0 \uB2E4\uC2DC \uC800\uC7A5\uD558\uBA74 \uAC31\uC2E0\uB429\uB2C8\uB2E4.",
6747
+ inputSchema: InterviewPrepInputSchema.shape,
6748
+ handler: (args) => {
6749
+ try {
6750
+ const prep = saveInterviewPrep(args);
6751
+ return ok(`\uBA74\uC811 \uC900\uBE44 \uC790\uB8CC\uB97C \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4 (\uC9C8\uBB38 ${prep.questions.length}\uAC1C).`, prep);
6752
+ } catch (e) {
6753
+ return fail(e instanceof Error ? e.message : "\uC800\uC7A5 \uC2E4\uD328");
6754
+ }
6755
+ }
6756
+ },
6757
+ /* --------------------------------------------------------------- exports */
6758
+ {
6759
+ name: "export_cover_letter",
6760
+ title: "\uC790\uAE30\uC18C\uAC1C\uC11C \uB0B4\uBCF4\uB0B4\uAE30",
6761
+ description: '\uC790\uAE30\uC18C\uAC1C\uC11C\uB97C Markdown \uB610\uB294 \uC778\uC1C4\uC6A9 HTML(\uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C PDF\uB85C \uC800\uC7A5) \uD30C\uC77C\uB85C \uB0B4\uBCF4\uB0B4 \uB370\uC774\uD130 \uD3F4\uB354\uC758 exports\uC5D0 \uC800\uC7A5\uD558\uACE0, \uD30C\uC77C \uACBD\uB85C\uC640 \uBCF8\uBB38\uC744 \uB3CC\uB824\uC90D\uB2C8\uB2E4. \uC0AC\uC6A9\uC790\uAC00 "\uC790\uC18C\uC11C \uD30C\uC77C\uB85C \uBC1B\uACE0 \uC2F6\uC5B4"\uB77C\uACE0 \uD560 \uB54C \uC0AC\uC6A9\uD558\uC138\uC694. \uB300\uC2DC\uBCF4\uB4DC\uC5D0\uC11C \uC9C1\uC811 \uB2E4\uC6B4\uB85C\uB4DC\uD560 \uC218\uB3C4 \uC788\uC2B5\uB2C8\uB2E4.',
6762
+ inputSchema: {
6763
+ cover_letter_id: external_exports.string(),
6764
+ format: external_exports.enum(["md", "html"]).optional().describe("\uAE30\uBCF8 md")
6765
+ },
6766
+ handler: (args) => {
6767
+ const cl = coverLetterRepo.get(args.cover_letter_id, true);
6768
+ if (!cl) return fail("\uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
6769
+ const job = cl.job_id ? jobRepo.get(cl.job_id) : null;
6770
+ const profile = profileRepo.get();
6771
+ const format = args.format ?? "md";
6772
+ const result = format === "html" ? coverLetterToHtml(cl, { job, profile }) : coverLetterToMarkdown(cl, { job, profile });
6773
+ const dir = getExportsDir();
6774
+ const filePath = path4.join(dir, result.filename);
6775
+ try {
6776
+ fs3.writeFileSync(filePath, result.content, "utf8");
6777
+ } catch (e) {
6778
+ return fail(`\uD30C\uC77C \uC800\uC7A5 \uC2E4\uD328: ${e instanceof Error ? e.message : e}`);
6779
+ }
6780
+ return ok(`'${cl.title}'\uB97C ${format.toUpperCase()}\uB85C \uB0B4\uBCF4\uB0C8\uC2B5\uB2C8\uB2E4.
6781
+ \uC800\uC7A5 \uC704\uCE58: ${filePath}`, {
6782
+ path: filePath,
6783
+ filename: result.filename,
6784
+ content: result.content
6785
+ });
6786
+ }
6787
+ },
6788
+ /* ----------------------------------------------------------- dashboard ui */
6789
+ {
6790
+ name: "open_dashboard",
6791
+ title: "\uB300\uC2DC\uBCF4\uB4DC \uC5F4\uAE30",
6792
+ description: "\uB85C\uCEEC CareerMate \uB300\uC2DC\uBCF4\uB4DC\uB97C \uC0AC\uC6A9\uC790\uC758 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC5FD\uB2C8\uB2E4. \uC11C\uBC84\uAC00 \uC2E4\uD589 \uC911\uC774 \uC544\uB2C8\uBA74 \uC790\uB3D9\uC73C\uB85C \uC2DC\uC791\uD569\uB2C8\uB2E4. \uC800\uC7A5 \uACB0\uACFC\uB97C \uC0AC\uC6A9\uC790\uAC00 \uB208\uC73C\uB85C \uD655\uC778\uD558\uAC8C \uD558\uACE0 \uC2F6\uC744 \uB54C \uD638\uCD9C\uD558\uC138\uC694(\uC608: \uC801\uD569\uB3C4 \uBD84\uC11D/\uC790\uC18C\uC11C \uC800\uC7A5 \uD6C4).",
6793
+ inputSchema: {},
6794
+ handler: async () => {
6795
+ const { url, running } = await resolveDashboardUrl();
6796
+ if (running) openInBrowser(url);
6797
+ return ok(
6798
+ running ? `\uB300\uC2DC\uBCF4\uB4DC\uB97C \uC5F4\uC5C8\uC2B5\uB2C8\uB2E4: ${url}` : `\uB300\uC2DC\uBCF4\uB4DC \uC8FC\uC18C: ${url} (\uC11C\uBC84\uAC00 \uC544\uC9C1 \uC2E4\uD589 \uC911\uC774 \uC544\uB2C8\uBA74 \uD504\uB85C\uC81D\uD2B8 \uD3F4\uB354\uC5D0\uC11C npm start \uB97C \uC2E4\uD589\uD558\uC138\uC694).`,
6799
+ { url, running }
6800
+ );
6801
+ }
6802
+ },
6803
+ {
6804
+ name: "open_application",
6805
+ title: "\uC9C0\uC6D0/\uACF5\uACE0 \uD398\uC774\uC9C0 \uC5F4\uAE30",
6806
+ description: "\uD2B9\uC815 \uACF5\uACE0\uC758 \uC9C0\uC6D0 \uC0C1\uC138 \uD398\uC774\uC9C0\uB97C \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC5FD\uB2C8\uB2E4. \uC801\uD569\uB3C4\xB7\uC790\uC18C\uC11C\xB7\uBA74\uC811 \uC900\uBE44\uB97C \uC0AC\uC6A9\uC790\uAC00 \uBC14\uB85C \uD655\uC778\uD558\uB3C4\uB85D \uC548\uB0B4\uD560 \uB54C \uC0AC\uC6A9\uD558\uC138\uC694.",
6807
+ inputSchema: { job_id: external_exports.string() },
6808
+ handler: async (args) => {
6809
+ const job = jobRepo.get(args.job_id);
6810
+ if (!job) return fail("\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
6811
+ const { url, running } = await resolveDashboardUrl(`/#/jobs/${args.job_id}`);
6812
+ if (running) openInBrowser(url);
6813
+ return ok(running ? `'${job.company} \xB7 ${job.position}' \uD398\uC774\uC9C0\uB97C \uC5F4\uC5C8\uC2B5\uB2C8\uB2E4: ${url}` : `\uD398\uC774\uC9C0 \uC8FC\uC18C: ${url}`, { url, running });
6814
+ }
6815
+ },
6816
+ /* ------------------------------------------------------------- activity */
6817
+ {
6818
+ name: "list_recent_activity",
6819
+ title: "\uCD5C\uADFC \uD65C\uB3D9 \uC870\uD68C",
6820
+ description: '\uCD5C\uADFC \uD65C\uB3D9 \uB0B4\uC5ED(\uACF5\uACE0 \uC800\uC7A5, \uC801\uD569\uB3C4 \uBD84\uC11D, \uC790\uC18C\uC11C \uBC84\uC804, \uC0C1\uD0DC \uBCC0\uACBD, \uBA74\uC811 \uC900\uBE44 \uB4F1)\uC744 \uB3CC\uB824\uC90D\uB2C8\uB2E4. \uC0AC\uC6A9\uC790\uAC00 "\uCD5C\uADFC\uC5D0 \uBB50 \uD588\uC9C0?"\uB77C\uACE0 \uBB3B\uAC70\uB098, \uC791\uC5C5\uC744 \uC774\uC5B4\uC11C \uC9C4\uD589\uD560 \uB9E5\uB77D\uC774 \uD544\uC694\uD560 \uB54C \uC0AC\uC6A9\uD558\uC138\uC694.',
6821
+ inputSchema: { limit: external_exports.number().optional().describe("\uAE30\uBCF8 20") },
6822
+ readOnly: true,
6823
+ handler: (args) => {
6824
+ const acts = listRecentActivity(args?.limit ?? 20);
6825
+ return ok(`\uCD5C\uADFC \uD65C\uB3D9 ${acts.length}\uAC74`, acts);
6826
+ }
6827
+ },
6828
+ /* ------------------------------------------------------- workflow guide */
6829
+ {
6830
+ name: "get_workflow_guide",
6831
+ title: "\uC6CC\uD06C\uD50C\uB85C\uC6B0 \uAC00\uC774\uB4DC",
6832
+ description: "CareerMate\uC758 \uD45C\uC900 \uC6CC\uD06C\uD50C\uB85C\uC6B0(\uC628\uBCF4\uB529, \uACF5\uACE0 \uBD84\uC11D, \uC790\uC18C\uC11C \uC791\uC131, \uC9C0\uC6D0 \uC0C1\uD0DC \uAD00\uB9AC, \uBA74\uC811 \uC900\uBE44)\uC758 \uB2E8\uACC4\uBCC4 \uC548\uB0B4\uB97C \uB3CC\uB824\uC90D\uB2C8\uB2E4. \uC5B4\uB5A4 \uC21C\uC11C\uB85C \uC5B4\uB5A4 \uB3C4\uAD6C\uB97C \uD638\uCD9C\uD574\uC57C \uD560\uC9C0 \uD655\uC778\uD560 \uB54C \uC0AC\uC6A9\uD558\uC138\uC694. workflow_id\uB97C \uC0DD\uB7B5\uD558\uBA74 \uC804\uCCB4 \uBAA9\uB85D\uC744 \uB3CC\uB824\uC90D\uB2C8\uB2E4.",
6833
+ inputSchema: {
6834
+ workflow_id: external_exports.enum(["onboarding", "analyze_job", "write_cover_letter", "manage_application_status", "prepare_interview"]).optional()
6835
+ },
6836
+ readOnly: true,
6837
+ handler: (args) => {
6838
+ if (args?.workflow_id) {
6839
+ const wf = getWorkflow(args.workflow_id);
6840
+ if (!wf) return fail("\uD574\uB2F9 \uC6CC\uD06C\uD50C\uB85C\uC6B0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
6841
+ return ok(renderWorkflowMarkdown(args.workflow_id) ?? wf.title, wf);
6842
+ }
6843
+ return ok(
6844
+ `\uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uC6CC\uD06C\uD50C\uB85C\uC6B0 ${WORKFLOWS.length}\uAC1C`,
6845
+ WORKFLOWS.map((w) => ({ id: w.id, title: w.title, trigger: w.trigger }))
6846
+ );
6847
+ }
6848
+ },
6849
+ /* --------------------------------------------------- writing style guide */
6850
+ {
6851
+ name: "get_writing_style_guide",
6852
+ title: "AI \uD2F0 \uC548 \uB098\uB294 \uAE00\uC4F0\uAE30 \uAC00\uC774\uB4DC",
6853
+ description: '\uC790\uAE30\uC18C\uAC1C\uC11C\xB7\uC790\uAE30 PR\xB7\uC9C0\uC6D0 \uBA54\uC77C \uB4F1 \uC0AC\uB78C\uC774 \uC4F4 \uB4EF\uD55C \uAE00\uC744 \uC791\uC131\uD558\uAE30 \uC9C1\uC804\uC5D0 \uD638\uCD9C\uD558\uC138\uC694. "AI\uAC00 \uC4F4 \uD2F0"\uAC00 \uB098\uB294 \uD55C\uAD6D\uC5B4 \uBB38\uC7A5 \uC2B5\uAD00(\uBC88\uC5ED\uD22C, \uD074\uB9AC\uC170, \uAE30\uACC4\uC801 \uBCD1\uB82C, \uC0C1\uD22C\uC801 \uC5F0\uACB0\uC5B4, \uADE0\uC77C\uD55C \uBB38\uC7A5 \uB9AC\uB4EC \uB4F1)\uC744 \uC81C\uAC70\uD558\uB294 \uC791\uC131 \uADDC\uCE59\uACFC \uC800\uC7A5 \uC804 \uC790\uAC00 \uC810\uAC80 \uD56D\uBAA9\uC744 \uB3CC\uB824\uC90D\uB2C8\uB2E4. \uC0AC\uC2E4\xB7\uC218\uCE58\xB7\uACE0\uC720\uBA85\uC0AC\uB294 \uADF8\uB300\uB85C \uB450\uACE0 \uBB38\uC7A5 \uACB0\uB9CC \uB2E4\uB4EC\uB3C4\uB85D \uC548\uB0B4\uD569\uB2C8\uB2E4. write_cover_letter / analyze_job \uC6CC\uD06C\uD50C\uB85C\uC6B0\uC5D0\uC11C \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC4F8 \uB54C \uBC18\uB4DC\uC2DC \uD568\uAED8 \uC801\uC6A9\uD558\uC138\uC694.',
6854
+ inputSchema: {},
6855
+ readOnly: true,
6856
+ handler: () => ok("AI \uD2F0 \uC548 \uB098\uB294 \uAE00\uC4F0\uAE30 \uAC00\uC774\uB4DC\uB97C \uC801\uC6A9\uD574 \uC791\uC131\uD558\uC138\uC694.", { guide: HUMANIZE_WRITING_GUIDE })
6857
+ }
6858
+ ];
6859
+
6860
+ // scripts/doctor.ts
6861
+ var ok2 = (s) => console.log(` \u2705 ${s}`);
6862
+ var warn = (s) => console.log(` \u26A0\uFE0F ${s}`);
6863
+ var info = (s) => console.log(` \xB7 ${s}`);
6864
+ function checkNode() {
6865
+ const [maj, min] = process.versions.node.split(".").map(Number);
6866
+ const good = maj > 22 || maj === 22 && min >= 5;
6867
+ if (good) ok2(`Node.js ${process.version} (node:sqlite \uB0B4\uC7A5 \uC0AC\uC6A9 \u2014 \uBCC4\uB3C4 \uBE4C\uB4DC \uBD88\uD544\uC694)`);
6868
+ else warn(`Node.js ${process.version} \u2014 22.5 \uC774\uC0C1\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. \uC5C5\uB370\uC774\uD2B8\uB97C \uAD8C\uC7A5\uD569\uB2C8\uB2E4.`);
6869
+ return good;
6870
+ }
6871
+ function main() {
6872
+ console.log("\nCareerMate \uC0C1\uD0DC \uC810\uAC80\n" + "\u2500".repeat(40));
6873
+ const nodeOk = checkNode();
6874
+ const dir = getDataDir();
6875
+ try {
6876
+ fs4.accessSync(dir, fs4.constants.W_OK);
6877
+ ok2(`\uB370\uC774\uD130 \uD3F4\uB354 \uC4F0\uAE30 \uAC00\uB2A5: ${dir}`);
6878
+ } catch {
6879
+ warn(`\uB370\uC774\uD130 \uD3F4\uB354\uC5D0 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${dir}`);
6880
+ }
6881
+ let dbOk = false;
6882
+ try {
6883
+ const db = getDb();
6884
+ const v = db.prepare(`SELECT value FROM _meta WHERE key='schema_version'`).get();
6885
+ ok2(`\uB370\uC774\uD130\uBCA0\uC774\uC2A4 \uC815\uC0C1 (\uC2A4\uD0A4\uB9C8 v${v?.value ?? "?"}) \u2014 ${getDbPath()}`);
6886
+ dbOk = true;
6887
+ } catch (e) {
6888
+ warn(`\uB370\uC774\uD130\uBCA0\uC774\uC2A4 \uC5F4\uAE30 \uC2E4\uD328: ${e instanceof Error ? e.message : e}`);
6889
+ }
6890
+ if (dbOk) {
6891
+ const counts = {
6892
+ \uD504\uB85C\uD544: profileRepo.get()?.name ? 1 : 0,
6893
+ \uACBD\uB825: experienceRepo.list().length,
6894
+ \uD504\uB85C\uC81D\uD2B8: projectRepo.list().length,
6895
+ \uAE30\uC220: skillRepo.list().length,
6896
+ \uBB38\uC11C: documentRepo.list().length,
6897
+ \uC790\uAE30\uC18C\uAC1C\uC11C: coverLetterRepo.list().length,
6898
+ \uACF5\uACE0: jobRepo.list().length,
6899
+ \uC9C0\uC6D0: applicationRepo.list().length,
6900
+ \uBA74\uC811\uC900\uBE44: interviewRepo.list().length
6901
+ };
6902
+ info("\uC800\uC7A5\uB41C \uB370\uC774\uD130: " + Object.entries(counts).map(([k, v]) => `${k} ${v}`).join(" \xB7 "));
6903
+ }
6904
+ ok2(`MCP \uB3C4\uAD6C ${TOOLS.length}\uAC1C \uB4F1\uB85D\uB428`);
6905
+ const rt = readRuntimeInfo();
6906
+ if (rt && isProcessAlive(rt.pid)) ok2(`\uB300\uC2DC\uBCF4\uB4DC \uC2E4\uD589 \uC911: ${rt.url}`);
6907
+ else info("\uB300\uC2DC\uBCF4\uB4DC\uAC00 \uC2E4\uD589 \uC911\uC774 \uC544\uB2D9\uB2C8\uB2E4. `npm start` \uB85C \uC2E4\uD589\uD558\uC138\uC694.");
6908
+ console.log("\u2500".repeat(40));
6909
+ console.log(nodeOk && dbOk ? "\uD575\uC2EC \uC810\uAC80 \uD1B5\uACFC \u2705\n" : "\uC77C\uBD80 \uD56D\uBAA9\uC744 \uD655\uC778\uD574 \uC8FC\uC138\uC694 \u26A0\uFE0F\n");
6910
+ process.exit(nodeOk && dbOk ? 0 : 1);
6911
+ }
6912
+ main();