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
@@ -12,6 +12,21 @@
12
12
  Light + dark (system preference or manual toggle).
13
13
  ========================================================================== */
14
14
 
15
+ /* Self-hosted brand font — the SAME face the dashboard ships (apps/web), so the
16
+ landing prose and the embedded dashboard screenshots read as one product.
17
+ Without this the page silently falls back to Segoe UI / 맑은 고딕 on machines
18
+ that don't have Pretendard installed. */
19
+ @font-face {
20
+ font-family: 'Pretendard Variable';
21
+ font-weight: 45 920;
22
+ font-style: normal;
23
+ font-display: swap;
24
+ /* relative URL: resolves under the site root (Vercel) AND under /install/ when
25
+ the local dashboard serves this same page. */
26
+ src: local('Pretendard Variable'),
27
+ url('fonts/PretendardVariable.woff2') format('woff2');
28
+ }
29
+
15
30
  :root {
16
31
  /* warm paper neutrals (light) */
17
32
  --bg: #faf8f4;
@@ -47,7 +62,7 @@
47
62
  --radius-lg: 12px;
48
63
  --maxw: 1000px;
49
64
 
50
- --font: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Malgun Gothic',
65
+ --font: 'Pretendard Variable', Pretendard, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Malgun Gothic',
51
66
  'Apple SD Gothic Neo', 'Noto Sans KR', Roboto, sans-serif;
52
67
  --mono: 'Cascadia Code', 'SFMono-Regular', 'JetBrains Mono', Consolas, 'D2Coding', ui-monospace, monospace;
53
68
  }
@@ -96,6 +111,7 @@ code { font-family: var(--mono); }
96
111
  ::selection { background: color-mix(in srgb, var(--accent) 22%, transparent); }
97
112
 
98
113
  .accent { color: var(--accent); }
114
+ .nowrap { white-space: nowrap; }
99
115
 
100
116
  /* ----------------------------------------------------------------- nav */
101
117
  .nav {
@@ -226,6 +242,74 @@ code { font-family: var(--mono); }
226
242
  .check strong { display: block; font-size: 14.5px; font-weight: 650; }
227
243
  .check span { display: block; color: var(--text-secondary); font-size: 13px; margin-top: 2px; }
228
244
 
245
+ /* ----------------------------------------------------------- dashboard preview */
246
+ /* A carousel of real dashboard screenshots, one browser-framed slide at a time.
247
+ Prev/next arrows + dots; light/dark variants both in the DOM, CSS shows the one
248
+ matching the page theme. Degrades to the first slide if JS is off. */
249
+ /* side padding reserves space for the arrows so they sit OUTSIDE the frame
250
+ (never covering the screenshot). The frame shrinks to fit; on small screens
251
+ the padding collapses and arrows overlap the edges instead. */
252
+ .carousel { position: relative; max-width: 936px; margin: 0 auto; padding: 0 54px; }
253
+ .carousel__viewport { overflow: hidden; border-radius: var(--radius-lg); }
254
+ .carousel__track { display: flex; transition: transform .42s cubic-bezier(.22,.61,.36,1); }
255
+ @media (prefers-reduced-motion: reduce) { .carousel__track { transition: none; } }
256
+ .slide { flex: 0 0 100%; min-width: 0; margin: 0; } /* reset <figure> default margin so each slide is exactly 100% (translateX math) */
257
+ .slide figcaption { margin-top: 14px; text-align: center; font-size: 13.5px; color: var(--text-secondary); }
258
+ .slide figcaption strong { color: var(--text); font-weight: 650; }
259
+
260
+ .browser {
261
+ border: 1px solid var(--border-strong); border-radius: var(--radius-lg); overflow: hidden;
262
+ background: var(--surface); box-shadow: var(--shadow-md);
263
+ }
264
+ .browser__bar {
265
+ display: flex; align-items: center; gap: 10px; height: 38px; padding: 0 14px;
266
+ background: var(--bg-subtle); border-bottom: 1px solid var(--border);
267
+ }
268
+ .browser__dots { display: inline-flex; gap: 6px; flex: none; }
269
+ .browser__dots i { width: 10px; height: 10px; border-radius: 999px; background: var(--border-strong); }
270
+ .browser__url {
271
+ flex: 1; max-width: 320px; margin: 0 auto;
272
+ font-family: var(--mono); font-size: 12px; color: var(--text-tertiary); text-align: center;
273
+ background: var(--surface); border: 1px solid var(--border); border-radius: 999px; padding: 3px 12px;
274
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
275
+ }
276
+ .browser__body { display: block; background: var(--bg); }
277
+ .browser__body img { width: 100%; height: auto; }
278
+
279
+ /* show the screenshot that matches the current theme (default page theme = dark) */
280
+ .shot__img { display: none; }
281
+ :root[data-theme='dark'] .shot__img--dark { display: block; }
282
+ :root:not([data-theme='dark']) .shot__img--light { display: block; }
283
+
284
+ /* prev / next arrows — overlap the frame edges, vertically centred on the frame */
285
+ .carousel__arrow {
286
+ position: absolute; top: 44%; transform: translateY(-50%);
287
+ width: 44px; height: 44px; border-radius: 999px; display: grid; place-items: center;
288
+ background: var(--surface); border: 1px solid var(--border-strong); color: var(--text);
289
+ box-shadow: var(--shadow-md); cursor: pointer; z-index: 2;
290
+ transition: background .15s, opacity .15s;
291
+ }
292
+ .carousel__arrow:hover { background: var(--surface-hover); }
293
+ .carousel__arrow:disabled { opacity: .3; cursor: default; box-shadow: none; }
294
+ .carousel__arrow svg { width: 20px; height: 20px; }
295
+ .carousel__arrow--prev { left: 0; }
296
+ .carousel__arrow--next { right: 0; }
297
+
298
+ /* dots */
299
+ .carousel__dots { display: flex; justify-content: center; gap: 8px; margin-top: 18px; }
300
+ .carousel__dot {
301
+ width: 8px; height: 8px; padding: 0; border: 0; border-radius: 999px;
302
+ background: var(--border-strong); cursor: pointer; transition: background .15s, width .15s;
303
+ }
304
+ .carousel__dot:hover { background: var(--text-tertiary); }
305
+ .carousel__dot.is-active { width: 22px; background: var(--accent); }
306
+
307
+ @media (max-width: 640px) {
308
+ .carousel { padding: 0; }
309
+ .carousel__arrow--prev { left: 6px; }
310
+ .carousel__arrow--next { right: 6px; }
311
+ }
312
+
229
313
  /* ----------------------------------------------------------------- install */
230
314
  /* install track (A: .mcpb / B: terminal) */
231
315
  .track {
@@ -287,8 +371,9 @@ kbd {
287
371
  color: var(--text-tertiary); padding: 9px 13px 0;
288
372
  }
289
373
  .code pre {
290
- margin: 0; padding: 11px 13px 13px; overflow-x: auto;
291
- font-family: var(--mono); font-size: 12.5px; line-height: 1.6; color: var(--code-text); white-space: pre;
374
+ margin: 0; padding: 11px 64px 13px 13px; overflow-x: auto;
375
+ font-family: var(--mono); font-size: 12.5px; line-height: 1.6; color: var(--code-text);
376
+ white-space: pre-wrap; overflow-wrap: anywhere;
292
377
  }
293
378
  .copy-btn {
294
379
  position: absolute; top: 7px; right: 7px; font-family: inherit; font-size: 12px; font-weight: 600;
@@ -327,6 +412,12 @@ kbd {
327
412
  .acc > summary:hover { background: var(--surface-hover); }
328
413
  .acc__hint { font-weight: 500; font-size: 12.5px; color: var(--text-tertiary); }
329
414
  .acc__body { padding: 0 17px 16px; }
415
+ .acc + .acc { margin-top: 10px; }
416
+ .track + .acc { margin-top: 16px; }
417
+ /* track-as-accordion: pill in summary + intro paragraph in body */
418
+ .acc > summary .track__tag { margin: 0; flex: none; }
419
+ .acc > summary { flex-wrap: wrap; }
420
+ .track-desc { color: var(--text-secondary); font-size: 14px; line-height: 1.6; margin: 0 0 12px; }
330
421
  .trouble { display: grid; grid-template-columns: repeat(2, 1fr); gap: 13px; }
331
422
  .trouble__item h4 { font-size: 14px; margin-bottom: 4px; }
332
423
  .trouble__item p { color: var(--text-secondary); font-size: 13px; }
package/dist/mcp.mjs CHANGED
@@ -13916,10 +13916,10 @@ function _endsWith(suffix, params) {
13916
13916
  suffix
13917
13917
  });
13918
13918
  }
13919
- function _overwrite(tx) {
13919
+ function _overwrite(tx2) {
13920
13920
  return new $ZodCheckOverwrite({
13921
13921
  check: "overwrite",
13922
- tx
13922
+ tx: tx2
13923
13923
  });
13924
13924
  }
13925
13925
  function _normalize(form) {
@@ -15041,7 +15041,7 @@ var ZodType2 = /* @__PURE__ */ $constructor("ZodType", (inst, def) => {
15041
15041
  inst.array = () => array(inst);
15042
15042
  inst.or = (arg) => union([inst, arg]);
15043
15043
  inst.and = (arg) => intersection(inst, arg);
15044
- inst.transform = (tx) => pipe(inst, transform(tx));
15044
+ inst.transform = (tx2) => pipe(inst, transform(tx2));
15045
15045
  inst.default = (def2) => _default(inst, def2);
15046
15046
  inst.prefault = (def2) => prefault(inst, def2);
15047
15047
  inst.catch = (params) => _catch(inst, params);
@@ -21300,6 +21300,123 @@ var MIGRATIONS = [
21300
21300
  CREATE INDEX IF NOT EXISTS idx_clv_letter ON cover_letter_versions(cover_letter_id);
21301
21301
  CREATE INDEX IF NOT EXISTS idx_cl_job ON cover_letters(job_id);
21302
21302
  CREATE INDEX IF NOT EXISTS idx_activities_created ON activities(created_at DESC);
21303
+ `,
21304
+ // v2 — add foreign keys so PRAGMA foreign_keys=ON is actually enforced.
21305
+ //
21306
+ // SQLite cannot ALTER a table to add constraints, so each table that needs an
21307
+ // FK is recreated (create new → copy → drop old → rename). The copy also
21308
+ // repairs pre-existing orphans: child rows whose parent is gone are dropped
21309
+ // (CASCADE intent) and dangling optional refs are nulled (SET NULL intent), so
21310
+ // a subsequent PRAGMA foreign_key_check stays clean. FKs are disabled for the
21311
+ // rebuild and the whole thing runs in one transaction (auto-rolled-back if the
21312
+ // process dies mid-migration, since the version bump happens only afterwards).
21313
+ `
21314
+ PRAGMA foreign_keys=OFF;
21315
+ BEGIN;
21316
+
21317
+ -- cover_letters.job_id -> jobs(id) ON DELETE SET NULL
21318
+ CREATE TABLE cover_letters_new (
21319
+ id TEXT PRIMARY KEY,
21320
+ title TEXT NOT NULL,
21321
+ job_id TEXT REFERENCES jobs(id) ON DELETE SET NULL,
21322
+ is_primary INTEGER NOT NULL DEFAULT 0,
21323
+ current_version_id TEXT,
21324
+ created_at TEXT NOT NULL,
21325
+ updated_at TEXT NOT NULL
21326
+ );
21327
+ INSERT INTO cover_letters_new (id,title,job_id,is_primary,current_version_id,created_at,updated_at)
21328
+ SELECT id,title,
21329
+ CASE WHEN job_id IN (SELECT id FROM jobs) THEN job_id ELSE NULL END,
21330
+ is_primary,current_version_id,created_at,updated_at
21331
+ FROM cover_letters;
21332
+ DROP TABLE cover_letters;
21333
+ ALTER TABLE cover_letters_new RENAME TO cover_letters;
21334
+
21335
+ -- cover_letter_versions.cover_letter_id -> cover_letters(id) ON DELETE CASCADE
21336
+ CREATE TABLE cover_letter_versions_new (
21337
+ id TEXT PRIMARY KEY,
21338
+ cover_letter_id TEXT NOT NULL REFERENCES cover_letters(id) ON DELETE CASCADE,
21339
+ version_no INTEGER NOT NULL,
21340
+ content TEXT NOT NULL,
21341
+ note TEXT,
21342
+ source TEXT NOT NULL DEFAULT 'ai',
21343
+ created_at TEXT NOT NULL
21344
+ );
21345
+ INSERT INTO cover_letter_versions_new (id,cover_letter_id,version_no,content,note,source,created_at)
21346
+ SELECT id,cover_letter_id,version_no,content,note,source,created_at
21347
+ FROM cover_letter_versions
21348
+ WHERE cover_letter_id IN (SELECT id FROM cover_letters);
21349
+ DROP TABLE cover_letter_versions;
21350
+ ALTER TABLE cover_letter_versions_new RENAME TO cover_letter_versions;
21351
+
21352
+ -- fit_analyses.job_id -> jobs(id) ON DELETE CASCADE
21353
+ CREATE TABLE fit_analyses_new (
21354
+ id TEXT PRIMARY KEY,
21355
+ job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
21356
+ score REAL,
21357
+ summary TEXT,
21358
+ strengths TEXT NOT NULL DEFAULT '[]',
21359
+ gaps TEXT NOT NULL DEFAULT '[]',
21360
+ matched_keywords TEXT NOT NULL DEFAULT '[]',
21361
+ missing_keywords TEXT NOT NULL DEFAULT '[]',
21362
+ recommendations TEXT NOT NULL DEFAULT '[]',
21363
+ created_at TEXT NOT NULL,
21364
+ updated_at TEXT NOT NULL
21365
+ );
21366
+ INSERT INTO fit_analyses_new (id,job_id,score,summary,strengths,gaps,matched_keywords,missing_keywords,recommendations,created_at,updated_at)
21367
+ SELECT id,job_id,score,summary,strengths,gaps,matched_keywords,missing_keywords,recommendations,created_at,updated_at
21368
+ FROM fit_analyses
21369
+ WHERE job_id IN (SELECT id FROM jobs);
21370
+ DROP TABLE fit_analyses;
21371
+ ALTER TABLE fit_analyses_new RENAME TO fit_analyses;
21372
+
21373
+ -- applications: job_id CASCADE, resume_id/cover_letter_id SET NULL
21374
+ CREATE TABLE applications_new (
21375
+ id TEXT PRIMARY KEY,
21376
+ job_id TEXT NOT NULL UNIQUE REFERENCES jobs(id) ON DELETE CASCADE,
21377
+ status TEXT NOT NULL DEFAULT 'draft',
21378
+ resume_id TEXT REFERENCES documents(id) ON DELETE SET NULL,
21379
+ cover_letter_id TEXT REFERENCES cover_letters(id) ON DELETE SET NULL,
21380
+ applied_at TEXT,
21381
+ notes TEXT,
21382
+ created_at TEXT NOT NULL,
21383
+ updated_at TEXT NOT NULL
21384
+ );
21385
+ INSERT INTO applications_new (id,job_id,status,resume_id,cover_letter_id,applied_at,notes,created_at,updated_at)
21386
+ SELECT id,job_id,status,
21387
+ CASE WHEN resume_id IN (SELECT id FROM documents) THEN resume_id ELSE NULL END,
21388
+ CASE WHEN cover_letter_id IN (SELECT id FROM cover_letters) THEN cover_letter_id ELSE NULL END,
21389
+ applied_at,notes,created_at,updated_at
21390
+ FROM applications
21391
+ WHERE job_id IN (SELECT id FROM jobs);
21392
+ DROP TABLE applications;
21393
+ ALTER TABLE applications_new RENAME TO applications;
21394
+
21395
+ -- interview_preps.job_id -> jobs(id) ON DELETE CASCADE
21396
+ CREATE TABLE interview_preps_new (
21397
+ id TEXT PRIMARY KEY,
21398
+ job_id TEXT NOT NULL UNIQUE REFERENCES jobs(id) ON DELETE CASCADE,
21399
+ questions TEXT NOT NULL DEFAULT '[]',
21400
+ star_guides TEXT NOT NULL DEFAULT '[]',
21401
+ self_introduction TEXT,
21402
+ notes TEXT,
21403
+ created_at TEXT NOT NULL,
21404
+ updated_at TEXT NOT NULL
21405
+ );
21406
+ INSERT INTO interview_preps_new (id,job_id,questions,star_guides,self_introduction,notes,created_at,updated_at)
21407
+ SELECT id,job_id,questions,star_guides,self_introduction,notes,created_at,updated_at
21408
+ FROM interview_preps
21409
+ WHERE job_id IN (SELECT id FROM jobs);
21410
+ DROP TABLE interview_preps;
21411
+ ALTER TABLE interview_preps_new RENAME TO interview_preps;
21412
+
21413
+ -- Recreate indexes dropped along with their tables.
21414
+ CREATE INDEX IF NOT EXISTS idx_fit_job ON fit_analyses(job_id);
21415
+ CREATE INDEX IF NOT EXISTS idx_clv_letter ON cover_letter_versions(cover_letter_id);
21416
+ CREATE INDEX IF NOT EXISTS idx_cl_job ON cover_letters(job_id);
21417
+
21418
+ COMMIT;
21419
+ PRAGMA foreign_keys=ON;
21303
21420
  `
21304
21421
  ];
21305
21422
  function migrate(db) {
@@ -21331,6 +21448,26 @@ function getDb() {
21331
21448
  _db = db;
21332
21449
  return db;
21333
21450
  }
21451
+ var _inTx = false;
21452
+ function tx(fn) {
21453
+ if (_inTx) return fn();
21454
+ const db = getDb();
21455
+ db.exec("BEGIN");
21456
+ _inTx = true;
21457
+ try {
21458
+ const result = fn();
21459
+ db.exec("COMMIT");
21460
+ return result;
21461
+ } catch (err) {
21462
+ try {
21463
+ db.exec("ROLLBACK");
21464
+ } catch {
21465
+ }
21466
+ throw err;
21467
+ } finally {
21468
+ _inTx = false;
21469
+ }
21470
+ }
21334
21471
  function toJson(value) {
21335
21472
  return JSON.stringify(value ?? null);
21336
21473
  }
@@ -21379,6 +21516,27 @@ var INTERVIEW_UNLOCK_STATUSES = [
21379
21516
  "interview",
21380
21517
  "final_passed"
21381
21518
  ];
21519
+ var ALLOWED_STATUS_TRANSITIONS = {
21520
+ draft: ["planned", "applied", "document_passed", "interview", "final_passed", "rejected", "on_hold"],
21521
+ planned: ["applied", "document_passed", "interview", "final_passed", "rejected", "on_hold"],
21522
+ applied: ["planned", "document_passed", "interview", "final_passed", "rejected", "on_hold"],
21523
+ document_passed: ["planned", "applied", "interview", "final_passed", "rejected", "on_hold"],
21524
+ interview: ["planned", "applied", "document_passed", "final_passed", "rejected", "on_hold"],
21525
+ final_passed: ["document_passed", "interview", "rejected", "on_hold"],
21526
+ rejected: ["draft", "planned", "applied", "document_passed", "interview", "final_passed", "on_hold"],
21527
+ on_hold: ["draft", "planned", "applied", "document_passed", "interview", "final_passed", "rejected"]
21528
+ };
21529
+ function canTransitionStatus(from, to) {
21530
+ if (from === to) return true;
21531
+ return ALLOWED_STATUS_TRANSITIONS[from]?.includes(to) ?? false;
21532
+ }
21533
+ function assertStatusTransition(from, to) {
21534
+ if (!canTransitionStatus(from, to)) {
21535
+ throw new Error(
21536
+ `'${APPLICATION_STATUS_LABELS[from]}'\uC5D0\uC11C '${APPLICATION_STATUS_LABELS[to]}'(\uC73C)\uB85C\uB294 \uC0C1\uD0DC\uB97C \uBC14\uAFC0 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`
21537
+ );
21538
+ }
21539
+ }
21382
21540
  var DOCUMENT_KINDS = ["resume", "career_description", "portfolio", "other"];
21383
21541
  var CONTENT_SOURCES = ["manual", "upload", "ai", "edit"];
21384
21542
  var ACTIVITY_TYPES = [
@@ -21996,34 +22154,38 @@ var documentRepo = {
21996
22154
  const ts = now();
21997
22155
  const id2 = newId("doc_");
21998
22156
  const db = getDb();
21999
- if (input.is_primary) {
22000
- db.prepare(`UPDATE documents SET is_primary=0 WHERE kind=?`).run(input.kind);
22001
- }
22002
- db.prepare(
22003
- `INSERT INTO documents (id,kind,title,content,source,is_primary,tags,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?)`
22004
- ).run(
22005
- id2,
22006
- input.kind,
22007
- input.title,
22008
- input.content ?? "",
22009
- input.source ?? "manual",
22010
- toBit(input.is_primary),
22011
- toJson(input.tags ?? []),
22012
- ts,
22013
- ts
22014
- );
22015
- return this.get(id2);
22157
+ return tx(() => {
22158
+ if (input.is_primary) {
22159
+ db.prepare(`UPDATE documents SET is_primary=0 WHERE kind=?`).run(input.kind);
22160
+ }
22161
+ db.prepare(
22162
+ `INSERT INTO documents (id,kind,title,content,source,is_primary,tags,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?)`
22163
+ ).run(
22164
+ id2,
22165
+ input.kind,
22166
+ input.title,
22167
+ input.content ?? "",
22168
+ input.source ?? "manual",
22169
+ toBit(input.is_primary),
22170
+ toJson(input.tags ?? []),
22171
+ ts,
22172
+ ts
22173
+ );
22174
+ return this.get(id2);
22175
+ });
22016
22176
  },
22017
22177
  update(id2, input) {
22018
22178
  const cur = this.get(id2);
22019
22179
  if (!cur) return null;
22020
22180
  const db = getDb();
22021
22181
  const m = { ...cur, ...input };
22022
- if (input.is_primary) db.prepare(`UPDATE documents SET is_primary=0 WHERE kind=?`).run(m.kind);
22023
- db.prepare(
22024
- `UPDATE documents SET kind=?,title=?,content=?,source=?,is_primary=?,tags=?,updated_at=? WHERE id=?`
22025
- ).run(m.kind, m.title, m.content, m.source, toBit(m.is_primary), toJson(m.tags), now(), id2);
22026
- return this.get(id2);
22182
+ return tx(() => {
22183
+ if (input.is_primary) db.prepare(`UPDATE documents SET is_primary=0 WHERE kind=?`).run(m.kind);
22184
+ db.prepare(
22185
+ `UPDATE documents SET kind=?,title=?,content=?,source=?,is_primary=?,tags=?,updated_at=? WHERE id=?`
22186
+ ).run(m.kind, m.title, m.content, m.source, toBit(m.is_primary), toJson(m.tags), now(), id2);
22187
+ return this.get(id2);
22188
+ });
22027
22189
  },
22028
22190
  remove(id2) {
22029
22191
  return getDb().prepare(`DELETE FROM documents WHERE id=?`).run(id2).changes > 0;
@@ -22072,37 +22234,41 @@ var coverLetterRepo = {
22072
22234
  const ts = now();
22073
22235
  const id2 = newId("cl_");
22074
22236
  const db = getDb();
22075
- if (opts.is_primary) db.prepare(`UPDATE cover_letters SET is_primary=0`).run();
22076
- db.prepare(
22077
- `INSERT INTO cover_letters (id,title,job_id,is_primary,current_version_id,created_at,updated_at) VALUES (?,?,?,?,?,?,?)`
22078
- ).run(id2, opts.title, opts.job_id ?? null, toBit(opts.is_primary), null, ts, ts);
22079
- return this.get(id2);
22237
+ return tx(() => {
22238
+ if (opts.is_primary) db.prepare(`UPDATE cover_letters SET is_primary=0`).run();
22239
+ db.prepare(
22240
+ `INSERT INTO cover_letters (id,title,job_id,is_primary,current_version_id,created_at,updated_at) VALUES (?,?,?,?,?,?,?)`
22241
+ ).run(id2, opts.title, opts.job_id ?? null, toBit(opts.is_primary), null, ts, ts);
22242
+ return this.get(id2);
22243
+ });
22080
22244
  },
22081
22245
  /** Append a new version. Creates the cover letter if cover_letter_id is omitted. */
22082
22246
  addVersion(opts) {
22083
22247
  const db = getDb();
22084
- let clId = opts.cover_letter_id;
22085
- if (!clId) {
22086
- const cl = this.create({ title: opts.title ?? "\uC790\uAE30\uC18C\uAC1C\uC11C", job_id: opts.job_id ?? null });
22087
- clId = cl.id;
22088
- }
22089
- const maxRow = db.prepare(`SELECT MAX(version_no) AS m FROM cover_letter_versions WHERE cover_letter_id=?`).get(clId);
22090
- const versionNo = (maxRow.m ?? 0) + 1;
22091
- const vid = newId("clv_");
22092
- const ts = now();
22093
- db.prepare(
22094
- `INSERT INTO cover_letter_versions (id,cover_letter_id,version_no,content,note,source,created_at) VALUES (?,?,?,?,?,?,?)`
22095
- ).run(vid, clId, versionNo, opts.content, opts.note ?? null, opts.source ?? "ai", ts);
22096
- const setCurrent = opts.set_current !== false;
22097
- if (setCurrent) {
22098
- db.prepare(`UPDATE cover_letters SET current_version_id=?, updated_at=? WHERE id=?`).run(vid, ts, clId);
22099
- } else {
22100
- db.prepare(`UPDATE cover_letters SET updated_at=? WHERE id=?`).run(ts, clId);
22101
- }
22102
- if (opts.title) db.prepare(`UPDATE cover_letters SET title=? WHERE id=?`).run(opts.title, clId);
22103
- if (opts.job_id !== void 0)
22104
- db.prepare(`UPDATE cover_letters SET job_id=? WHERE id=?`).run(opts.job_id, clId);
22105
- return { coverLetter: this.get(clId), version: mapVersion(db.prepare(`SELECT * FROM cover_letter_versions WHERE id=?`).get(vid)) };
22248
+ return tx(() => {
22249
+ let clId = opts.cover_letter_id;
22250
+ if (!clId) {
22251
+ const cl = this.create({ title: opts.title ?? "\uC790\uAE30\uC18C\uAC1C\uC11C", job_id: opts.job_id ?? null });
22252
+ clId = cl.id;
22253
+ }
22254
+ const maxRow = db.prepare(`SELECT MAX(version_no) AS m FROM cover_letter_versions WHERE cover_letter_id=?`).get(clId);
22255
+ const versionNo = (maxRow.m ?? 0) + 1;
22256
+ const vid = newId("clv_");
22257
+ const ts = now();
22258
+ db.prepare(
22259
+ `INSERT INTO cover_letter_versions (id,cover_letter_id,version_no,content,note,source,created_at) VALUES (?,?,?,?,?,?,?)`
22260
+ ).run(vid, clId, versionNo, opts.content, opts.note ?? null, opts.source ?? "ai", ts);
22261
+ const setCurrent = opts.set_current !== false;
22262
+ if (setCurrent) {
22263
+ db.prepare(`UPDATE cover_letters SET current_version_id=?, updated_at=? WHERE id=?`).run(vid, ts, clId);
22264
+ } else {
22265
+ db.prepare(`UPDATE cover_letters SET updated_at=? WHERE id=?`).run(ts, clId);
22266
+ }
22267
+ if (opts.title) db.prepare(`UPDATE cover_letters SET title=? WHERE id=?`).run(opts.title, clId);
22268
+ if (opts.job_id !== void 0)
22269
+ db.prepare(`UPDATE cover_letters SET job_id=? WHERE id=?`).run(opts.job_id, clId);
22270
+ return { coverLetter: this.get(clId), version: mapVersion(db.prepare(`SELECT * FROM cover_letter_versions WHERE id=?`).get(vid)) };
22271
+ });
22106
22272
  },
22107
22273
  setCurrentVersion(coverLetterId, versionId) {
22108
22274
  getDb().prepare(`UPDATE cover_letters SET current_version_id=?, updated_at=? WHERE id=?`).run(versionId, now(), coverLetterId);
@@ -22110,14 +22276,18 @@ var coverLetterRepo = {
22110
22276
  },
22111
22277
  setPrimary(id2) {
22112
22278
  const db = getDb();
22113
- db.prepare(`UPDATE cover_letters SET is_primary=0`).run();
22114
- db.prepare(`UPDATE cover_letters SET is_primary=1, updated_at=? WHERE id=?`).run(now(), id2);
22115
- return this.get(id2);
22279
+ return tx(() => {
22280
+ db.prepare(`UPDATE cover_letters SET is_primary=0`).run();
22281
+ db.prepare(`UPDATE cover_letters SET is_primary=1, updated_at=? WHERE id=?`).run(now(), id2);
22282
+ return this.get(id2);
22283
+ });
22116
22284
  },
22117
22285
  remove(id2) {
22118
22286
  const db = getDb();
22119
- db.prepare(`DELETE FROM cover_letter_versions WHERE cover_letter_id=?`).run(id2);
22120
- return db.prepare(`DELETE FROM cover_letters WHERE id=?`).run(id2).changes > 0;
22287
+ return tx(() => {
22288
+ db.prepare(`DELETE FROM cover_letter_versions WHERE cover_letter_id=?`).run(id2);
22289
+ return db.prepare(`DELETE FROM cover_letters WHERE id=?`).run(id2).changes > 0;
22290
+ });
22121
22291
  }
22122
22292
  };
22123
22293
  function mapJob(r) {
@@ -22202,10 +22372,12 @@ var jobRepo = {
22202
22372
  },
22203
22373
  remove(id2) {
22204
22374
  const db = getDb();
22205
- db.prepare(`DELETE FROM fit_analyses WHERE job_id=?`).run(id2);
22206
- db.prepare(`DELETE FROM applications WHERE job_id=?`).run(id2);
22207
- db.prepare(`DELETE FROM interview_preps WHERE job_id=?`).run(id2);
22208
- return db.prepare(`DELETE FROM jobs WHERE id=?`).run(id2).changes > 0;
22375
+ return tx(() => {
22376
+ db.prepare(`DELETE FROM fit_analyses WHERE job_id=?`).run(id2);
22377
+ db.prepare(`DELETE FROM applications WHERE job_id=?`).run(id2);
22378
+ db.prepare(`DELETE FROM interview_preps WHERE job_id=?`).run(id2);
22379
+ return db.prepare(`DELETE FROM jobs WHERE id=?`).run(id2).changes > 0;
22380
+ });
22209
22381
  }
22210
22382
  };
22211
22383
  function mapFit(r) {
@@ -22312,6 +22484,7 @@ var applicationRepo = {
22312
22484
  },
22313
22485
  upsert(input) {
22314
22486
  const app = this.ensure(input.job_id, input.status ?? "draft");
22487
+ if (input.status && input.status !== app.status) assertStatusTransition(app.status, input.status);
22315
22488
  const m = { ...app, ...input };
22316
22489
  getDb().prepare(
22317
22490
  `UPDATE applications SET status=?,resume_id=?,cover_letter_id=?,applied_at=?,notes=?,updated_at=? WHERE id=?`
@@ -22320,6 +22493,7 @@ var applicationRepo = {
22320
22493
  },
22321
22494
  setStatus(jobId, status, note) {
22322
22495
  const app = this.ensure(jobId);
22496
+ assertStatusTransition(app.status, status);
22323
22497
  const appliedAt = status === "applied" && !app.applied_at ? now() : app.applied_at;
22324
22498
  const notes = note ? [app.notes, note].filter(Boolean).join("\n") : app.notes;
22325
22499
  getDb().prepare(`UPDATE applications SET status=?, applied_at=?, notes=?, updated_at=? WHERE id=?`).run(status, appliedAt, notes, now(), app.id);
@@ -22508,6 +22682,21 @@ function addResume(input) {
22508
22682
  activityRepo.log("resume_added", `${doc.title} \uBB38\uC11C\uB97C \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "document", doc.id);
22509
22683
  return doc;
22510
22684
  }
22685
+ function addExperience(input) {
22686
+ const exp = experienceRepo.add(input);
22687
+ activityRepo.log("profile_updated", `\uACBD\uB825(${exp.company})\uC744 \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "experience", exp.id);
22688
+ return exp;
22689
+ }
22690
+ function addProject(input) {
22691
+ const prj = projectRepo.add(input);
22692
+ activityRepo.log("profile_updated", `\uD504\uB85C\uC81D\uD2B8(${prj.name})\uB97C \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "project", prj.id);
22693
+ return prj;
22694
+ }
22695
+ function addSkill(input) {
22696
+ const skill = skillRepo.add(input);
22697
+ activityRepo.log("profile_updated", `\uAE30\uC220(${skill.name})\uC744 \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "skill", skill.id);
22698
+ return skill;
22699
+ }
22511
22700
  function saveJobPosting(input, id2) {
22512
22701
  const job = jobRepo.upsert(input, id2);
22513
22702
  const application = applicationRepo.ensure(job.id, "draft");
@@ -22565,7 +22754,13 @@ function updateApplicationStatus(jobId, status, note) {
22565
22754
  function saveInterviewPrep(input) {
22566
22755
  const job = jobRepo.get(input.job_id);
22567
22756
  if (!job) throw new Error(`\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.job_id}`);
22568
- applicationRepo.ensure(job.id);
22757
+ const app = applicationRepo.getByJob(job.id);
22758
+ if (!app || !INTERVIEW_UNLOCK_STATUSES.includes(app.status)) {
22759
+ const current = app ? APPLICATION_STATUS_LABELS[app.status] : "\uBBF8\uC9C0\uC6D0";
22760
+ throw new Error(
22761
+ `\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.`
22762
+ );
22763
+ }
22569
22764
  const prep = interviewRepo.save(input);
22570
22765
  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);
22571
22766
  return prep;
@@ -22763,11 +22958,8 @@ function renderWorkflowMarkdown(id2) {
22763
22958
  function escapeHtml(input) {
22764
22959
  return (input ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
22765
22960
  }
22766
- function looksLikeHtml(s) {
22767
- return /<\/?(p|h[1-6]|ul|ol|li|div|section|article|br|hr|strong|em|a|table)\b/i.test(s);
22768
- }
22769
- function toPrintableHtml(title, bodyMarkdownOrHtml) {
22770
- const body = looksLikeHtml(bodyMarkdownOrHtml) ? bodyMarkdownOrHtml : markdownToHtml(bodyMarkdownOrHtml);
22961
+ function toPrintableHtml(title, bodyMarkdown) {
22962
+ const body = markdownToHtml(bodyMarkdown);
22771
22963
  const safeTitle = escapeHtml(title);
22772
22964
  return `<!doctype html>
22773
22965
  <html lang="ko">
@@ -23384,6 +23576,72 @@ ${ONBOARDING_PROMPT}`,
23384
23576
  return ok(`\uBB38\uC11C ${docs.length}\uAC74`, docs);
23385
23577
  }
23386
23578
  },
23579
+ /* ----------------------------------------------------------- experiences */
23580
+ {
23581
+ name: "add_experience",
23582
+ title: "\uACBD\uB825 \uCD94\uAC00",
23583
+ 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.",
23584
+ inputSchema: ExperienceInputSchema.shape,
23585
+ handler: (args) => {
23586
+ const exp = addExperience(args);
23587
+ return ok(`\uACBD\uB825 '${exp.company}${exp.role ? ` \xB7 ${exp.role}` : ""}'\uC744(\uB97C) \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, exp);
23588
+ }
23589
+ },
23590
+ {
23591
+ name: "get_experiences",
23592
+ title: "\uACBD\uB825 \uBAA9\uB85D \uC870\uD68C",
23593
+ 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.)",
23594
+ inputSchema: {},
23595
+ readOnly: true,
23596
+ handler: () => {
23597
+ const list = experienceRepo.list();
23598
+ return ok(`\uACBD\uB825 ${list.length}\uAC74`, list);
23599
+ }
23600
+ },
23601
+ /* -------------------------------------------------------------- projects */
23602
+ {
23603
+ name: "add_project",
23604
+ title: "\uD504\uB85C\uC81D\uD2B8 \uCD94\uAC00",
23605
+ 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.",
23606
+ inputSchema: ProjectInputSchema.shape,
23607
+ handler: (args) => {
23608
+ const prj = addProject(args);
23609
+ return ok(`\uD504\uB85C\uC81D\uD2B8 '${prj.name}'\uC744(\uB97C) \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, prj);
23610
+ }
23611
+ },
23612
+ {
23613
+ name: "get_projects",
23614
+ title: "\uD504\uB85C\uC81D\uD2B8 \uBAA9\uB85D \uC870\uD68C",
23615
+ 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.)",
23616
+ inputSchema: {},
23617
+ readOnly: true,
23618
+ handler: () => {
23619
+ const list = projectRepo.list();
23620
+ return ok(`\uD504\uB85C\uC81D\uD2B8 ${list.length}\uAC74`, list);
23621
+ }
23622
+ },
23623
+ /* ---------------------------------------------------------------- skills */
23624
+ {
23625
+ name: "add_skill",
23626
+ title: "\uAE30\uC220\uC2A4\uD0DD \uCD94\uAC00",
23627
+ 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.",
23628
+ inputSchema: SkillInputSchema.shape,
23629
+ handler: (args) => {
23630
+ const skill = addSkill(args);
23631
+ return ok(`\uAE30\uC220 '${skill.name}'\uC744(\uB97C) \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, skill);
23632
+ }
23633
+ },
23634
+ {
23635
+ name: "get_skills",
23636
+ title: "\uAE30\uC220\uC2A4\uD0DD \uBAA9\uB85D \uC870\uD68C",
23637
+ 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.)",
23638
+ inputSchema: {},
23639
+ readOnly: true,
23640
+ handler: () => {
23641
+ const list = skillRepo.list();
23642
+ return ok(`\uAE30\uC220 ${list.length}\uAC74`, list);
23643
+ }
23644
+ },
23387
23645
  /* ---------------------------------------------------------- cover letters */
23388
23646
  {
23389
23647
  name: "add_cover_letter",
@@ -23661,7 +23919,7 @@ ${ONBOARDING_PROMPT}`,
23661
23919
  ];
23662
23920
 
23663
23921
  // apps/mcp/src/index.ts
23664
- var APP_VERSION = "0.1.0";
23922
+ var APP_VERSION = true ? "0.1.1" : "0.0.0-dev";
23665
23923
  async function main() {
23666
23924
  getDb();
23667
23925
  const server = new McpServer(