careermate 0.1.0

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 (124) hide show
  1. package/README.md +256 -0
  2. package/THIRD_PARTY_NOTICES.md +40 -0
  3. package/apps/mcp/src/index.ts +66 -0
  4. package/apps/web/DESIGN_GUIDE.md +105 -0
  5. package/apps/web/UI_CONTRACT.md +44 -0
  6. package/apps/web/public/app.js +118 -0
  7. package/apps/web/public/fonts/PretendardVariable.woff2 +0 -0
  8. package/apps/web/public/index.html +41 -0
  9. package/apps/web/public/lib.js +282 -0
  10. package/apps/web/public/pages/applications.js +98 -0
  11. package/apps/web/public/pages/documents.js +446 -0
  12. package/apps/web/public/pages/home.js +263 -0
  13. package/apps/web/public/pages/interview.js +230 -0
  14. package/apps/web/public/pages/jobs.js +494 -0
  15. package/apps/web/public/pages/profile.js +576 -0
  16. package/apps/web/public/pages/settings.js +233 -0
  17. package/apps/web/public/styles.css +426 -0
  18. package/apps/web/src/exports.ts +68 -0
  19. package/apps/web/src/http.ts +180 -0
  20. package/apps/web/src/index.ts +49 -0
  21. package/apps/web/src/info.ts +50 -0
  22. package/apps/web/src/routes.ts +350 -0
  23. package/apps/web/src/security.ts +102 -0
  24. package/apps/web/src/server.ts +141 -0
  25. package/apps/web/src/settings.ts +88 -0
  26. package/bin/careermate.mjs +74 -0
  27. package/dist/careermate.mcpb +0 -0
  28. package/dist/install-page/index.html +474 -0
  29. package/dist/install-page/style.css +391 -0
  30. package/dist/install-page/vercel.json +20 -0
  31. package/dist/mcp-smoke.err +3 -0
  32. package/dist/mcp.mjs +23704 -0
  33. package/dist/mcpb-stage/README.md +219 -0
  34. package/dist/mcpb-stage/dist/install-page/index.html +434 -0
  35. package/dist/mcpb-stage/dist/install-page/style.css +407 -0
  36. package/dist/mcpb-stage/dist/install-page/vercel.json +20 -0
  37. package/dist/mcpb-stage/dist/mcp.mjs +23704 -0
  38. package/dist/mcpb-stage/dist/public/app.js +118 -0
  39. package/dist/mcpb-stage/dist/public/fonts/PretendardVariable.woff2 +0 -0
  40. package/dist/mcpb-stage/dist/public/index.html +41 -0
  41. package/dist/mcpb-stage/dist/public/lib.js +282 -0
  42. package/dist/mcpb-stage/dist/public/pages/applications.js +98 -0
  43. package/dist/mcpb-stage/dist/public/pages/documents.js +446 -0
  44. package/dist/mcpb-stage/dist/public/pages/home.js +263 -0
  45. package/dist/mcpb-stage/dist/public/pages/interview.js +230 -0
  46. package/dist/mcpb-stage/dist/public/pages/jobs.js +494 -0
  47. package/dist/mcpb-stage/dist/public/pages/profile.js +576 -0
  48. package/dist/mcpb-stage/dist/public/pages/settings.js +233 -0
  49. package/dist/mcpb-stage/dist/public/styles.css +420 -0
  50. package/dist/mcpb-stage/dist/web.mjs +7240 -0
  51. package/dist/mcpb-stage/manifest.json +40 -0
  52. package/dist/public/app.js +118 -0
  53. package/dist/public/fonts/PretendardVariable.woff2 +0 -0
  54. package/dist/public/index.html +41 -0
  55. package/dist/public/lib.js +282 -0
  56. package/dist/public/pages/applications.js +98 -0
  57. package/dist/public/pages/documents.js +446 -0
  58. package/dist/public/pages/home.js +263 -0
  59. package/dist/public/pages/interview.js +230 -0
  60. package/dist/public/pages/jobs.js +494 -0
  61. package/dist/public/pages/profile.js +576 -0
  62. package/dist/public/pages/settings.js +233 -0
  63. package/dist/public/styles.css +426 -0
  64. package/dist/web.mjs +7240 -0
  65. package/docs/ARCHITECTURE.md +208 -0
  66. package/docs/CHANGES_V1.md +103 -0
  67. package/docs/DATA_MODEL.md +460 -0
  68. package/docs/DECISIONS.md +277 -0
  69. package/docs/DEMO.md +242 -0
  70. package/docs/INSTALL.md +148 -0
  71. package/docs/INSTALL_AND_USAGE.md +99 -0
  72. package/docs/MCP_TOOLS.md +233 -0
  73. package/docs/ROADMAP.md +134 -0
  74. package/docs/START_WORKFLOW.md +125 -0
  75. package/docs/SUPPORTED_AI_APPS.md +60 -0
  76. package/docs/TODO.md +57 -0
  77. package/docs/UX_NOTES.md +247 -0
  78. package/docs/WORKFLOWS.md +200 -0
  79. package/install-page/index.html +474 -0
  80. package/install-page/style.css +391 -0
  81. package/install-page/vercel.json +20 -0
  82. package/package.json +68 -0
  83. package/packages/core/src/context.ts +74 -0
  84. package/packages/core/src/index.ts +8 -0
  85. package/packages/core/src/onboarding.ts +81 -0
  86. package/packages/core/src/services.ts +146 -0
  87. package/packages/core/src/summary.ts +104 -0
  88. package/packages/db/src/connection.ts +46 -0
  89. package/packages/db/src/index.ts +22 -0
  90. package/packages/db/src/paths.ts +41 -0
  91. package/packages/db/src/repositories.ts +828 -0
  92. package/packages/db/src/runtime.ts +58 -0
  93. package/packages/db/src/schema.ts +189 -0
  94. package/packages/exporters/src/html.ts +113 -0
  95. package/packages/exporters/src/index.ts +364 -0
  96. package/packages/exporters/src/markdown.ts +178 -0
  97. package/packages/mcp-tools/src/bridge.ts +83 -0
  98. package/packages/mcp-tools/src/index.ts +8 -0
  99. package/packages/mcp-tools/src/result.ts +49 -0
  100. package/packages/mcp-tools/src/tools.ts +455 -0
  101. package/packages/parsers/src/html.ts +86 -0
  102. package/packages/parsers/src/index.ts +228 -0
  103. package/packages/parsers/src/keywords.ts +151 -0
  104. package/packages/prompts/src/humanize.ts +59 -0
  105. package/packages/prompts/src/index.ts +82 -0
  106. package/packages/prompts/src/install.ts +43 -0
  107. package/packages/prompts/src/onboarding.ts +35 -0
  108. package/packages/prompts/src/system.ts +53 -0
  109. package/packages/shared/src/enums.ts +103 -0
  110. package/packages/shared/src/index.ts +18 -0
  111. package/packages/shared/src/schemas.ts +398 -0
  112. package/packages/workflows/src/definitions.ts +107 -0
  113. package/packages/workflows/src/index.ts +39 -0
  114. package/scripts/build-dist.mjs +62 -0
  115. package/scripts/build-mcpb.mjs +70 -0
  116. package/scripts/doctor.ts +81 -0
  117. package/scripts/init.ts +342 -0
  118. package/scripts/mcp-probe.ts +55 -0
  119. package/scripts/migrate.ts +6 -0
  120. package/scripts/run.mjs +33 -0
  121. package/scripts/seed.ts +129 -0
  122. package/scripts/test.ts +117 -0
  123. package/scripts/ui-smoke.ts +73 -0
  124. package/tsconfig.json +29 -0
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Application services — the write-side use-cases shared by the HTTP API and the
3
+ * MCP server. Centralizing them here guarantees both interfaces behave
4
+ * identically and that every meaningful change is recorded in the activity feed.
5
+ *
6
+ * PRIVACY RULE: activity summaries and logs may reference titles, companies and
7
+ * positions, but MUST NEVER contain résumé or cover-letter body text.
8
+ */
9
+ import {
10
+ type JobInput,
11
+ type JobRecord,
12
+ type FitAnalysisInput,
13
+ type FitAnalysisRecord,
14
+ type CoverLetterVersionInput,
15
+ type ApplicationStatus,
16
+ type ApplicationRecord,
17
+ type InterviewPrepInput,
18
+ type InterviewPrepRecord,
19
+ type DocumentInput,
20
+ type DocumentRecord,
21
+ type ProfileInput,
22
+ type ProfileRecord,
23
+ APPLICATION_STATUS_LABELS,
24
+ INTERVIEW_UNLOCK_STATUSES,
25
+ } from '@careermate/shared';
26
+ import {
27
+ jobRepo,
28
+ fitRepo,
29
+ coverLetterRepo,
30
+ applicationRepo,
31
+ interviewRepo,
32
+ documentRepo,
33
+ profileRepo,
34
+ activityRepo,
35
+ } from '@careermate/db';
36
+
37
+ /* ----------------------------------------------------------------- Profile */
38
+
39
+ export function saveProfile(input: ProfileInput): ProfileRecord {
40
+ const p = profileRepo.save(input);
41
+ activityRepo.log('profile_updated', `프로필을 ${p.name ? `(${p.name}) ` : ''}저장했습니다.`, 'profile', p.id);
42
+ return p;
43
+ }
44
+
45
+ /* ---------------------------------------------------------------- Resumes */
46
+
47
+ export function addResume(input: Omit<DocumentInput, 'kind'> & { kind?: DocumentInput['kind'] }): DocumentRecord {
48
+ const doc = documentRepo.add({ ...input, kind: input.kind ?? 'resume' });
49
+ activityRepo.log('resume_added', `${doc.title} 문서를 추가했습니다.`, 'document', doc.id);
50
+ return doc;
51
+ }
52
+
53
+ /* -------------------------------------------------------------------- Jobs */
54
+
55
+ /** Save (or update) a posting and make sure an application row tracks it. */
56
+ export function saveJobPosting(input: JobInput, id?: string): { job: JobRecord; application: ApplicationRecord } {
57
+ const job = jobRepo.upsert(input, id);
58
+ const application = applicationRepo.ensure(job.id, 'draft');
59
+ activityRepo.log('job_saved', `${job.company} · ${job.position} 공고를 저장했습니다.`, 'job', job.id);
60
+ return { job, application };
61
+ }
62
+
63
+ /* ------------------------------------------------------------ Fit analysis */
64
+
65
+ export function saveFitAnalysis(input: FitAnalysisInput): FitAnalysisRecord {
66
+ const job = jobRepo.get(input.job_id);
67
+ if (!job) throw new Error(`공고를 찾을 수 없습니다: ${input.job_id}. 먼저 save_job_posting으로 공고를 저장하세요.`);
68
+ applicationRepo.ensure(job.id, 'draft');
69
+ const fit = fitRepo.save(input);
70
+ const scoreText = fit.score != null ? ` (적합도 ${fit.score}점)` : '';
71
+ activityRepo.log('fit_analysis_saved', `${job.company} · ${job.position} 적합도 분석을 저장했습니다${scoreText}.`, 'fit_analysis', fit.id);
72
+ return fit;
73
+ }
74
+
75
+ /* ----------------------------------------------------------- Cover letters */
76
+
77
+ export function saveCoverLetterVersion(input: CoverLetterVersionInput) {
78
+ const { coverLetter, version } = coverLetterRepo.addVersion({
79
+ cover_letter_id: input.cover_letter_id,
80
+ title: input.title,
81
+ job_id: input.job_id ?? null,
82
+ content: input.content,
83
+ note: input.note,
84
+ source: input.source ?? 'ai',
85
+ set_current: input.set_current,
86
+ });
87
+ // Link the application to this cover letter when tied to a job.
88
+ if (coverLetter.job_id) {
89
+ const app = applicationRepo.getByJob(coverLetter.job_id);
90
+ if (app && !app.cover_letter_id) {
91
+ applicationRepo.upsert({ job_id: coverLetter.job_id, cover_letter_id: coverLetter.id });
92
+ }
93
+ }
94
+ activityRepo.log(
95
+ 'cover_letter_version_saved',
96
+ `${coverLetter.title} 자기소개서 v${version.version_no}를 저장했습니다.`,
97
+ 'cover_letter',
98
+ coverLetter.id,
99
+ );
100
+ return { coverLetter, version };
101
+ }
102
+
103
+ /* ------------------------------------------------- Application status flow */
104
+
105
+ export interface StatusChangeResult {
106
+ application: ApplicationRecord;
107
+ job: JobRecord | null;
108
+ interview_unlocked: boolean;
109
+ hint: string | null;
110
+ }
111
+
112
+ export function updateApplicationStatus(
113
+ jobId: string,
114
+ status: ApplicationStatus,
115
+ note?: string,
116
+ ): StatusChangeResult {
117
+ const job = jobRepo.get(jobId);
118
+ if (!job) throw new Error(`공고를 찾을 수 없습니다: ${jobId}`);
119
+ const application = applicationRepo.setStatus(jobId, status, note);
120
+ activityRepo.log(
121
+ 'application_status_changed',
122
+ `${job.company} · ${job.position} 상태를 '${APPLICATION_STATUS_LABELS[status]}'(으)로 변경했습니다.`,
123
+ 'application',
124
+ application.id,
125
+ );
126
+
127
+ const interview_unlocked = INTERVIEW_UNLOCK_STATUSES.includes(status);
128
+ const hasPrep = !!interviewRepo.getByJob(jobId);
129
+ const hint =
130
+ interview_unlocked && !hasPrep
131
+ ? '서류 단계를 통과했어요. 이 공고 기준으로 예상 면접 질문과 1분 자기소개를 준비할까요? (save_interview_prep)'
132
+ : null;
133
+
134
+ return { application, job, interview_unlocked, hint };
135
+ }
136
+
137
+ /* --------------------------------------------------------- Interview prep */
138
+
139
+ export function saveInterviewPrep(input: InterviewPrepInput): InterviewPrepRecord {
140
+ const job = jobRepo.get(input.job_id);
141
+ if (!job) throw new Error(`공고를 찾을 수 없습니다: ${input.job_id}`);
142
+ applicationRepo.ensure(job.id);
143
+ const prep = interviewRepo.save(input);
144
+ activityRepo.log('interview_prep_saved', `${job.company} · ${job.position} 면접 준비 자료를 저장했습니다.`, 'interview_prep', prep.id);
145
+ return prep;
146
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Home dashboard summary + recent activity. Read-only aggregation that answers
3
+ * "what should I do right now?" at a glance.
4
+ */
5
+ import {
6
+ type ApplicationStatus,
7
+ APPLICATION_STATUS_LABELS,
8
+ INTERVIEW_UNLOCK_STATUSES,
9
+ type JobRecord,
10
+ type ApplicationRecord,
11
+ type CoverLetterRecord,
12
+ type ActivityRecord,
13
+ } from '@careermate/shared';
14
+ import {
15
+ jobRepo,
16
+ applicationRepo,
17
+ coverLetterRepo,
18
+ interviewRepo,
19
+ fitRepo,
20
+ activityRepo,
21
+ } from '@careermate/db';
22
+ import { getOnboardingStatus } from './onboarding.ts';
23
+
24
+ export interface JobWithMeta extends JobRecord {
25
+ status: ApplicationStatus;
26
+ status_label: string;
27
+ fit_score: number | null;
28
+ }
29
+
30
+ export interface HomeSummary {
31
+ onboarding: ReturnType<typeof getOnboardingStatus>;
32
+ counts: {
33
+ jobs: number;
34
+ active_applications: number;
35
+ cover_letters: number;
36
+ interview_pending: number;
37
+ };
38
+ status_breakdown: { status: ApplicationStatus; label: string; count: number }[];
39
+ recent_jobs: JobWithMeta[];
40
+ in_progress: { application: ApplicationRecord; job: JobRecord | null; status_label: string }[];
41
+ recent_cover_letters: CoverLetterRecord[];
42
+ interview_todo: { job: JobRecord; status_label: string }[];
43
+ recent_activity: ActivityRecord[];
44
+ }
45
+
46
+ const ACTIVE: ApplicationStatus[] = ['draft', 'planned', 'applied', 'document_passed', 'interview'];
47
+
48
+ export function jobWithMeta(job: JobRecord): JobWithMeta {
49
+ const app = applicationRepo.getByJob(job.id);
50
+ const fit = fitRepo.getByJob(job.id);
51
+ const status = app?.status ?? 'draft';
52
+ return { ...job, status, status_label: APPLICATION_STATUS_LABELS[status], fit_score: fit?.score ?? null };
53
+ }
54
+
55
+ export function getHomeSummary(): HomeSummary {
56
+ const onboarding = getOnboardingStatus();
57
+ const jobs = jobRepo.list();
58
+ const applications = applicationRepo.list();
59
+ const coverLetters = coverLetterRepo.list();
60
+
61
+ const status_breakdown = (Object.keys(APPLICATION_STATUS_LABELS) as ApplicationStatus[]).map((status) => ({
62
+ status,
63
+ label: APPLICATION_STATUS_LABELS[status],
64
+ count: applications.filter((a) => a.status === status).length,
65
+ }));
66
+
67
+ const active = applications.filter((a) => ACTIVE.includes(a.status));
68
+
69
+ // Interview to-do: passed-to-interview stages without saved prep yet.
70
+ const interview_todo = applications
71
+ .filter((a) => INTERVIEW_UNLOCK_STATUSES.includes(a.status) && !interviewRepo.getByJob(a.job_id))
72
+ .map((a) => {
73
+ const job = jobRepo.get(a.job_id);
74
+ return job ? { job, status_label: APPLICATION_STATUS_LABELS[a.status] } : null;
75
+ })
76
+ .filter((x): x is { job: JobRecord; status_label: string } => x !== null);
77
+
78
+ return {
79
+ onboarding,
80
+ counts: {
81
+ jobs: jobs.length,
82
+ active_applications: active.length,
83
+ cover_letters: coverLetters.length,
84
+ interview_pending: interview_todo.length,
85
+ },
86
+ status_breakdown,
87
+ recent_jobs: jobs.slice(0, 6).map(jobWithMeta),
88
+ in_progress: active.slice(0, 6).map((application) => {
89
+ const job = jobRepo.get(application.job_id);
90
+ return {
91
+ application,
92
+ job,
93
+ status_label: APPLICATION_STATUS_LABELS[application.status],
94
+ };
95
+ }),
96
+ recent_cover_letters: coverLetters.slice(0, 5),
97
+ interview_todo,
98
+ recent_activity: activityRepo.recent(10),
99
+ };
100
+ }
101
+
102
+ export function listRecentActivity(limit = 20): ActivityRecord[] {
103
+ return activityRepo.recent(limit);
104
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Single shared DatabaseSync connection.
3
+ *
4
+ * Both the web server and the MCP server import this. SQLite with WAL handles
5
+ * the two-process concurrency (dashboard editing + AI writing) cleanly.
6
+ */
7
+ import { DatabaseSync } from 'node:sqlite';
8
+ import { getDbPath } from './paths.ts';
9
+ import { migrate } from './schema.ts';
10
+
11
+ let _db: DatabaseSync | null = null;
12
+
13
+ export function getDb(): DatabaseSync {
14
+ if (_db) return _db;
15
+ const db = new DatabaseSync(getDbPath());
16
+ db.exec('PRAGMA journal_mode = WAL;');
17
+ db.exec('PRAGMA foreign_keys = ON;');
18
+ db.exec('PRAGMA busy_timeout = 5000;');
19
+ migrate(db);
20
+ _db = db;
21
+ return db;
22
+ }
23
+
24
+ export function closeDb(): void {
25
+ if (_db) {
26
+ _db.close();
27
+ _db = null;
28
+ }
29
+ }
30
+
31
+ /** Helpers for the JSON-text columns. */
32
+ export function toJson(value: unknown): string {
33
+ return JSON.stringify(value ?? null);
34
+ }
35
+
36
+ export function fromJson<T>(text: string | null | undefined, fallback: T): T {
37
+ if (text == null || text === '') return fallback;
38
+ try {
39
+ return JSON.parse(text) as T;
40
+ } catch {
41
+ return fallback;
42
+ }
43
+ }
44
+
45
+ export const toBit = (b: boolean | undefined | null): number => (b ? 1 : 0);
46
+ export const fromBit = (n: number | null | undefined): boolean => Number(n) === 1;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @careermate/db — SQLite persistence (built on Node's flag-free `node:sqlite`,
3
+ * so there is no native module to compile). Exposes a shared connection,
4
+ * migrations, and one repository object per entity.
5
+ */
6
+ export * from './paths.ts';
7
+ export * from './runtime.ts';
8
+ export * from './connection.ts';
9
+ export { migrate, MIGRATIONS } from './schema.ts';
10
+ export {
11
+ profileRepo,
12
+ experienceRepo,
13
+ projectRepo,
14
+ skillRepo,
15
+ documentRepo,
16
+ coverLetterRepo,
17
+ jobRepo,
18
+ fitRepo,
19
+ applicationRepo,
20
+ interviewRepo,
21
+ activityRepo,
22
+ } from './repositories.ts';
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Resolves where CareerMate keeps its local data. Everything is on the user's
3
+ * machine — no cloud, no external transfer. The location is overridable so power
4
+ * users can relocate it and the Settings page can display it.
5
+ *
6
+ * Priority:
7
+ * 1. CAREERMATE_DATA_DIR env var (absolute path)
8
+ * 2. ~/.careermate (default)
9
+ */
10
+ import os from 'node:os';
11
+ import path from 'node:path';
12
+ import fs from 'node:fs';
13
+
14
+ export function getDataDir(): string {
15
+ const override = process.env.CAREERMATE_DATA_DIR?.trim();
16
+ const dir = override && override.length > 0 ? override : path.join(os.homedir(), '.careermate');
17
+ fs.mkdirSync(dir, { recursive: true });
18
+ return dir;
19
+ }
20
+
21
+ export function getDbPath(): string {
22
+ return path.join(getDataDir(), 'careermate.sqlite');
23
+ }
24
+
25
+ export function getExportsDir(): string {
26
+ const dir = path.join(getDataDir(), 'exports');
27
+ fs.mkdirSync(dir, { recursive: true });
28
+ return dir;
29
+ }
30
+
31
+ export function getUploadsDir(): string {
32
+ const dir = path.join(getDataDir(), 'uploads');
33
+ fs.mkdirSync(dir, { recursive: true });
34
+ return dir;
35
+ }
36
+
37
+ export function getBackupsDir(): string {
38
+ const dir = path.join(getDataDir(), 'backups');
39
+ fs.mkdirSync(dir, { recursive: true });
40
+ return dir;
41
+ }