careermate 0.0.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 (137) hide show
  1. package/AGENTS.md +33 -0
  2. package/INSTALL.md +230 -0
  3. package/LICENSE +21 -0
  4. package/README.md +532 -0
  5. package/THIRD_PARTY_NOTICES.md +57 -0
  6. package/bin/careermate.mjs +77 -0
  7. package/dist/career-os/README.md +78 -0
  8. package/dist/career-os/eop/cover-letter.md +46 -0
  9. package/dist/career-os/eop/fit-analysis.md +48 -0
  10. package/dist/career-os/eop/interview-prep.md +39 -0
  11. package/dist/career-os/eop/job-analysis.md +42 -0
  12. package/dist/career-os/eop/profile-extraction.md +37 -0
  13. package/dist/career-os/knowledge/ats.md +131 -0
  14. package/dist/career-os/knowledge/company-research.md +137 -0
  15. package/dist/career-os/knowledge/cover-letter.md +135 -0
  16. package/dist/career-os/knowledge/fit-matching.md +121 -0
  17. package/dist/career-os/knowledge/human-writing.md +118 -0
  18. package/dist/career-os/knowledge/interview-behavioral.md +132 -0
  19. package/dist/career-os/knowledge/interview-technical.md +126 -0
  20. package/dist/career-os/knowledge/linkedin-profile.md +152 -0
  21. package/dist/career-os/knowledge/networking-referrals.md +166 -0
  22. package/dist/career-os/knowledge/offer-evaluation-decision.md +121 -0
  23. package/dist/career-os/knowledge/onboarding-first-90-days.md +126 -0
  24. package/dist/career-os/knowledge/portfolio.md +121 -0
  25. package/dist/career-os/knowledge/recruiter-screen.md +105 -0
  26. package/dist/career-os/knowledge/rejection-triage-iteration.md +115 -0
  27. package/dist/career-os/knowledge/resume.md +123 -0
  28. package/dist/career-os/knowledge/salary-negotiation.md +123 -0
  29. package/dist/career-os/knowledge/verifiers/ats-compat.md +64 -0
  30. package/dist/career-os/knowledge/verifiers/consistency.md +61 -0
  31. package/dist/career-os/knowledge/verifiers/human-voice.md +82 -0
  32. package/dist/career-os/knowledge/verifiers/recency-staleness.md +72 -0
  33. package/dist/career-os/knowledge/verifiers/responsiveness-on-target.md +82 -0
  34. package/dist/career-os/knowledge/verifiers/truthfulness.md +67 -0
  35. package/dist/doctor.mjs +79094 -0
  36. package/dist/mcp.mjs +96108 -0
  37. package/dist/migrate.mjs +4735 -0
  38. package/dist/pdf.worker.min.mjs +28 -0
  39. package/dist/public/app.js +150 -0
  40. package/dist/public/fonts/PretendardVariable.woff2 +0 -0
  41. package/dist/public/icon-180.png +0 -0
  42. package/dist/public/index.html +52 -0
  43. package/dist/public/lib.js +394 -0
  44. package/dist/public/pages/applications.js +107 -0
  45. package/dist/public/pages/documents.js +467 -0
  46. package/dist/public/pages/home.js +261 -0
  47. package/dist/public/pages/interview.js +227 -0
  48. package/dist/public/pages/jobs.js +493 -0
  49. package/dist/public/pages/profile.js +556 -0
  50. package/dist/public/pages/settings.js +240 -0
  51. package/dist/public/styles.css +498 -0
  52. package/dist/site/analytics.js +97 -0
  53. package/dist/site/favicon.svg +1 -0
  54. package/dist/site/fonts/PretendardVariable.woff2 +0 -0
  55. package/dist/site/icon-180.png +0 -0
  56. package/dist/site/icon-512.png +0 -0
  57. package/dist/site/index.html +598 -0
  58. package/dist/site/landing.js +54 -0
  59. package/dist/site/llms-install.txt +217 -0
  60. package/dist/site/robots.txt +4 -0
  61. package/dist/site/sitemap.xml +21 -0
  62. package/dist/site/start.html +326 -0
  63. package/dist/site/style.css +550 -0
  64. package/dist/site/vercel.json +20 -0
  65. package/dist/web.mjs +46852 -0
  66. package/docs/FAQ.md +101 -0
  67. package/docs/MCP_TOOLS.md +311 -0
  68. package/docs/README.md +28 -0
  69. package/docs/START_WORKFLOW.md +137 -0
  70. package/docs/SUPPORTED_AI_APPS.md +112 -0
  71. package/docs/WORKFLOWS.md +202 -0
  72. package/docs/career-os/README.md +78 -0
  73. package/docs/career-os/eop/cover-letter.md +46 -0
  74. package/docs/career-os/eop/fit-analysis.md +48 -0
  75. package/docs/career-os/eop/interview-prep.md +39 -0
  76. package/docs/career-os/eop/job-analysis.md +42 -0
  77. package/docs/career-os/eop/profile-extraction.md +37 -0
  78. package/docs/career-os/knowledge/ats.md +131 -0
  79. package/docs/career-os/knowledge/company-research.md +137 -0
  80. package/docs/career-os/knowledge/cover-letter.md +135 -0
  81. package/docs/career-os/knowledge/fit-matching.md +121 -0
  82. package/docs/career-os/knowledge/human-writing.md +118 -0
  83. package/docs/career-os/knowledge/interview-behavioral.md +132 -0
  84. package/docs/career-os/knowledge/interview-technical.md +126 -0
  85. package/docs/career-os/knowledge/linkedin-profile.md +152 -0
  86. package/docs/career-os/knowledge/networking-referrals.md +166 -0
  87. package/docs/career-os/knowledge/offer-evaluation-decision.md +121 -0
  88. package/docs/career-os/knowledge/onboarding-first-90-days.md +126 -0
  89. package/docs/career-os/knowledge/portfolio.md +121 -0
  90. package/docs/career-os/knowledge/recruiter-screen.md +105 -0
  91. package/docs/career-os/knowledge/rejection-triage-iteration.md +115 -0
  92. package/docs/career-os/knowledge/resume.md +123 -0
  93. package/docs/career-os/knowledge/salary-negotiation.md +123 -0
  94. package/docs/career-os/knowledge/verifiers/ats-compat.md +64 -0
  95. package/docs/career-os/knowledge/verifiers/consistency.md +61 -0
  96. package/docs/career-os/knowledge/verifiers/human-voice.md +82 -0
  97. package/docs/career-os/knowledge/verifiers/recency-staleness.md +72 -0
  98. package/docs/career-os/knowledge/verifiers/responsiveness-on-target.md +82 -0
  99. package/docs/career-os/knowledge/verifiers/truthfulness.md +67 -0
  100. package/package.json +89 -0
  101. package/packages/core/src/context.ts +74 -0
  102. package/packages/core/src/index.ts +8 -0
  103. package/packages/core/src/onboarding.ts +81 -0
  104. package/packages/core/src/services.ts +187 -0
  105. package/packages/core/src/summary.ts +104 -0
  106. package/packages/db/src/connection.ts +76 -0
  107. package/packages/db/src/index.ts +23 -0
  108. package/packages/db/src/paths.ts +52 -0
  109. package/packages/db/src/repositories.ts +891 -0
  110. package/packages/db/src/runtime.ts +58 -0
  111. package/packages/db/src/schema.ts +307 -0
  112. package/packages/exporters/src/html.ts +110 -0
  113. package/packages/exporters/src/index.ts +364 -0
  114. package/packages/exporters/src/markdown.ts +178 -0
  115. package/packages/knowledge/src/index.ts +168 -0
  116. package/packages/mcp-tools/src/bridge.ts +106 -0
  117. package/packages/mcp-tools/src/index.ts +17 -0
  118. package/packages/mcp-tools/src/result.ts +49 -0
  119. package/packages/mcp-tools/src/tools.ts +747 -0
  120. package/packages/mcp-tools/src/update.ts +330 -0
  121. package/packages/parsers/src/documents.ts +533 -0
  122. package/packages/parsers/src/html.ts +86 -0
  123. package/packages/parsers/src/index.ts +231 -0
  124. package/packages/parsers/src/keywords.ts +151 -0
  125. package/packages/prompts/src/humanize.ts +59 -0
  126. package/packages/prompts/src/index.ts +82 -0
  127. package/packages/prompts/src/install.ts +43 -0
  128. package/packages/prompts/src/onboarding.ts +35 -0
  129. package/packages/prompts/src/system.ts +57 -0
  130. package/packages/shared/src/build-info.ts +22 -0
  131. package/packages/shared/src/enums.ts +142 -0
  132. package/packages/shared/src/index.ts +19 -0
  133. package/packages/shared/src/schemas.ts +453 -0
  134. package/packages/workflows/src/definitions.ts +109 -0
  135. package/packages/workflows/src/index.ts +39 -0
  136. package/scripts/init.ts +371 -0
  137. package/tsconfig.json +32 -0
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Runtime handshake between the two local processes.
3
+ *
4
+ * The web server and the MCP server run independently (the MCP server is
5
+ * launched by the AI client). They find each other through a small JSON file in
6
+ * the data dir: the web server writes its live URL/port/pid on boot; the MCP
7
+ * server reads it so tools like `open_dashboard` can point the browser at the
8
+ * right place — without any network discovery.
9
+ */
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import { getDataDir } from './paths.ts';
13
+
14
+ export interface RuntimeInfo {
15
+ url: string;
16
+ port: number;
17
+ pid: number;
18
+ started_at: string;
19
+ }
20
+
21
+ function runtimeFile(): string {
22
+ return path.join(getDataDir(), 'server.json');
23
+ }
24
+
25
+ export function writeRuntimeInfo(info: RuntimeInfo): void {
26
+ try {
27
+ fs.writeFileSync(runtimeFile(), JSON.stringify(info, null, 2), 'utf8');
28
+ } catch {
29
+ /* non-fatal */
30
+ }
31
+ }
32
+
33
+ export function readRuntimeInfo(): RuntimeInfo | null {
34
+ try {
35
+ const raw = fs.readFileSync(runtimeFile(), 'utf8');
36
+ return JSON.parse(raw) as RuntimeInfo;
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ /** True if a process with this pid is currently alive. */
43
+ export function isProcessAlive(pid: number): boolean {
44
+ try {
45
+ process.kill(pid, 0);
46
+ return true;
47
+ } catch (e) {
48
+ return (e as NodeJS.ErrnoException).code === 'EPERM';
49
+ }
50
+ }
51
+
52
+ export function clearRuntimeInfo(): void {
53
+ try {
54
+ fs.unlinkSync(runtimeFile());
55
+ } catch {
56
+ /* ignore */
57
+ }
58
+ }
@@ -0,0 +1,307 @@
1
+ /**
2
+ * SQLite schema + lightweight migration runner.
3
+ *
4
+ * Migrations are an ordered list of SQL strings. The current version is tracked
5
+ * in `_meta`. Applying is idempotent and safe to run on every process start, so
6
+ * both the web server and the MCP server can call `migrate()` on boot and share
7
+ * the same database file without coordination.
8
+ */
9
+ import type { DatabaseSync } from 'node:sqlite';
10
+
11
+ export const MIGRATIONS: string[] = [
12
+ // v1 — initial schema
13
+ `
14
+ CREATE TABLE IF NOT EXISTS profile (
15
+ id TEXT PRIMARY KEY,
16
+ name TEXT, email TEXT, phone TEXT, location TEXT,
17
+ headline TEXT, summary TEXT,
18
+ desired_roles TEXT NOT NULL DEFAULT '[]',
19
+ desired_conditions TEXT,
20
+ preferred_tone TEXT,
21
+ emphasis_points TEXT NOT NULL DEFAULT '[]',
22
+ links TEXT NOT NULL DEFAULT '[]',
23
+ created_at TEXT NOT NULL,
24
+ updated_at TEXT NOT NULL
25
+ );
26
+
27
+ CREATE TABLE IF NOT EXISTS experiences (
28
+ id TEXT PRIMARY KEY,
29
+ company TEXT NOT NULL,
30
+ role TEXT,
31
+ employment_type TEXT,
32
+ start_date TEXT,
33
+ end_date TEXT,
34
+ is_current INTEGER NOT NULL DEFAULT 0,
35
+ description TEXT,
36
+ achievements TEXT NOT NULL DEFAULT '[]',
37
+ tech TEXT NOT NULL DEFAULT '[]',
38
+ order_index INTEGER NOT NULL DEFAULT 0,
39
+ created_at TEXT NOT NULL,
40
+ updated_at TEXT NOT NULL
41
+ );
42
+
43
+ CREATE TABLE IF NOT EXISTS projects (
44
+ id TEXT PRIMARY KEY,
45
+ name TEXT NOT NULL,
46
+ role TEXT,
47
+ description TEXT,
48
+ highlights TEXT NOT NULL DEFAULT '[]',
49
+ tech TEXT NOT NULL DEFAULT '[]',
50
+ url TEXT,
51
+ start_date TEXT,
52
+ end_date TEXT,
53
+ order_index INTEGER NOT NULL DEFAULT 0,
54
+ created_at TEXT NOT NULL,
55
+ updated_at TEXT NOT NULL
56
+ );
57
+
58
+ CREATE TABLE IF NOT EXISTS skills (
59
+ id TEXT PRIMARY KEY,
60
+ name TEXT NOT NULL,
61
+ category TEXT,
62
+ level TEXT,
63
+ years REAL,
64
+ order_index INTEGER NOT NULL DEFAULT 0,
65
+ created_at TEXT NOT NULL,
66
+ updated_at TEXT NOT NULL
67
+ );
68
+
69
+ CREATE TABLE IF NOT EXISTS documents (
70
+ id TEXT PRIMARY KEY,
71
+ kind TEXT NOT NULL,
72
+ title TEXT NOT NULL,
73
+ content TEXT NOT NULL DEFAULT '',
74
+ source TEXT NOT NULL DEFAULT 'manual',
75
+ is_primary INTEGER NOT NULL DEFAULT 0,
76
+ tags TEXT NOT NULL DEFAULT '[]',
77
+ created_at TEXT NOT NULL,
78
+ updated_at TEXT NOT NULL
79
+ );
80
+
81
+ CREATE TABLE IF NOT EXISTS cover_letters (
82
+ id TEXT PRIMARY KEY,
83
+ title TEXT NOT NULL,
84
+ job_id TEXT,
85
+ is_primary INTEGER NOT NULL DEFAULT 0,
86
+ current_version_id TEXT,
87
+ created_at TEXT NOT NULL,
88
+ updated_at TEXT NOT NULL
89
+ );
90
+
91
+ CREATE TABLE IF NOT EXISTS cover_letter_versions (
92
+ id TEXT PRIMARY KEY,
93
+ cover_letter_id TEXT NOT NULL,
94
+ version_no INTEGER NOT NULL,
95
+ content TEXT NOT NULL,
96
+ note TEXT,
97
+ source TEXT NOT NULL DEFAULT 'ai',
98
+ created_at TEXT NOT NULL
99
+ );
100
+
101
+ CREATE TABLE IF NOT EXISTS jobs (
102
+ id TEXT PRIMARY KEY,
103
+ company TEXT NOT NULL,
104
+ position TEXT NOT NULL,
105
+ url TEXT,
106
+ location TEXT,
107
+ employment_type TEXT,
108
+ description TEXT,
109
+ requirements TEXT NOT NULL DEFAULT '[]',
110
+ keywords TEXT NOT NULL DEFAULT '[]',
111
+ deadline TEXT,
112
+ source TEXT,
113
+ created_at TEXT NOT NULL,
114
+ updated_at TEXT NOT NULL
115
+ );
116
+
117
+ CREATE TABLE IF NOT EXISTS fit_analyses (
118
+ id TEXT PRIMARY KEY,
119
+ job_id TEXT NOT NULL,
120
+ score REAL,
121
+ summary TEXT,
122
+ strengths TEXT NOT NULL DEFAULT '[]',
123
+ gaps TEXT NOT NULL DEFAULT '[]',
124
+ matched_keywords TEXT NOT NULL DEFAULT '[]',
125
+ missing_keywords TEXT NOT NULL DEFAULT '[]',
126
+ recommendations TEXT NOT NULL DEFAULT '[]',
127
+ created_at TEXT NOT NULL,
128
+ updated_at TEXT NOT NULL
129
+ );
130
+
131
+ CREATE TABLE IF NOT EXISTS applications (
132
+ id TEXT PRIMARY KEY,
133
+ job_id TEXT NOT NULL UNIQUE,
134
+ status TEXT NOT NULL DEFAULT 'draft',
135
+ resume_id TEXT,
136
+ cover_letter_id TEXT,
137
+ applied_at TEXT,
138
+ notes TEXT,
139
+ created_at TEXT NOT NULL,
140
+ updated_at TEXT NOT NULL
141
+ );
142
+
143
+ CREATE TABLE IF NOT EXISTS interview_preps (
144
+ id TEXT PRIMARY KEY,
145
+ job_id TEXT NOT NULL UNIQUE,
146
+ questions TEXT NOT NULL DEFAULT '[]',
147
+ star_guides TEXT NOT NULL DEFAULT '[]',
148
+ self_introduction TEXT,
149
+ notes TEXT,
150
+ created_at TEXT NOT NULL,
151
+ updated_at TEXT NOT NULL
152
+ );
153
+
154
+ CREATE TABLE IF NOT EXISTS activities (
155
+ id TEXT PRIMARY KEY,
156
+ type TEXT NOT NULL,
157
+ entity_type TEXT,
158
+ entity_id TEXT,
159
+ summary TEXT NOT NULL,
160
+ created_at TEXT NOT NULL
161
+ );
162
+
163
+ CREATE INDEX IF NOT EXISTS idx_fit_job ON fit_analyses(job_id);
164
+ CREATE INDEX IF NOT EXISTS idx_clv_letter ON cover_letter_versions(cover_letter_id);
165
+ CREATE INDEX IF NOT EXISTS idx_cl_job ON cover_letters(job_id);
166
+ CREATE INDEX IF NOT EXISTS idx_activities_created ON activities(created_at DESC);
167
+ `,
168
+
169
+ // v2 — add foreign keys so PRAGMA foreign_keys=ON is actually enforced.
170
+ //
171
+ // SQLite cannot ALTER a table to add constraints, so each table that needs an
172
+ // FK is recreated (create new → copy → drop old → rename). The copy also
173
+ // repairs pre-existing orphans: child rows whose parent is gone are dropped
174
+ // (CASCADE intent) and dangling optional refs are nulled (SET NULL intent), so
175
+ // a subsequent PRAGMA foreign_key_check stays clean. FKs are disabled for the
176
+ // rebuild and the whole thing runs in one transaction (auto-rolled-back if the
177
+ // process dies mid-migration, since the version bump happens only afterwards).
178
+ `
179
+ PRAGMA foreign_keys=OFF;
180
+ BEGIN;
181
+
182
+ -- cover_letters.job_id -> jobs(id) ON DELETE SET NULL
183
+ CREATE TABLE cover_letters_new (
184
+ id TEXT PRIMARY KEY,
185
+ title TEXT NOT NULL,
186
+ job_id TEXT REFERENCES jobs(id) ON DELETE SET NULL,
187
+ is_primary INTEGER NOT NULL DEFAULT 0,
188
+ current_version_id TEXT,
189
+ created_at TEXT NOT NULL,
190
+ updated_at TEXT NOT NULL
191
+ );
192
+ INSERT INTO cover_letters_new (id,title,job_id,is_primary,current_version_id,created_at,updated_at)
193
+ SELECT id,title,
194
+ CASE WHEN job_id IN (SELECT id FROM jobs) THEN job_id ELSE NULL END,
195
+ is_primary,current_version_id,created_at,updated_at
196
+ FROM cover_letters;
197
+ DROP TABLE cover_letters;
198
+ ALTER TABLE cover_letters_new RENAME TO cover_letters;
199
+
200
+ -- cover_letter_versions.cover_letter_id -> cover_letters(id) ON DELETE CASCADE
201
+ CREATE TABLE cover_letter_versions_new (
202
+ id TEXT PRIMARY KEY,
203
+ cover_letter_id TEXT NOT NULL REFERENCES cover_letters(id) ON DELETE CASCADE,
204
+ version_no INTEGER NOT NULL,
205
+ content TEXT NOT NULL,
206
+ note TEXT,
207
+ source TEXT NOT NULL DEFAULT 'ai',
208
+ created_at TEXT NOT NULL
209
+ );
210
+ INSERT INTO cover_letter_versions_new (id,cover_letter_id,version_no,content,note,source,created_at)
211
+ SELECT id,cover_letter_id,version_no,content,note,source,created_at
212
+ FROM cover_letter_versions
213
+ WHERE cover_letter_id IN (SELECT id FROM cover_letters);
214
+ DROP TABLE cover_letter_versions;
215
+ ALTER TABLE cover_letter_versions_new RENAME TO cover_letter_versions;
216
+
217
+ -- fit_analyses.job_id -> jobs(id) ON DELETE CASCADE
218
+ CREATE TABLE fit_analyses_new (
219
+ id TEXT PRIMARY KEY,
220
+ job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
221
+ score REAL,
222
+ summary TEXT,
223
+ strengths TEXT NOT NULL DEFAULT '[]',
224
+ gaps TEXT NOT NULL DEFAULT '[]',
225
+ matched_keywords TEXT NOT NULL DEFAULT '[]',
226
+ missing_keywords TEXT NOT NULL DEFAULT '[]',
227
+ recommendations TEXT NOT NULL DEFAULT '[]',
228
+ created_at TEXT NOT NULL,
229
+ updated_at TEXT NOT NULL
230
+ );
231
+ INSERT INTO fit_analyses_new (id,job_id,score,summary,strengths,gaps,matched_keywords,missing_keywords,recommendations,created_at,updated_at)
232
+ SELECT id,job_id,score,summary,strengths,gaps,matched_keywords,missing_keywords,recommendations,created_at,updated_at
233
+ FROM fit_analyses
234
+ WHERE job_id IN (SELECT id FROM jobs);
235
+ DROP TABLE fit_analyses;
236
+ ALTER TABLE fit_analyses_new RENAME TO fit_analyses;
237
+
238
+ -- applications: job_id CASCADE, resume_id/cover_letter_id SET NULL
239
+ CREATE TABLE applications_new (
240
+ id TEXT PRIMARY KEY,
241
+ job_id TEXT NOT NULL UNIQUE REFERENCES jobs(id) ON DELETE CASCADE,
242
+ status TEXT NOT NULL DEFAULT 'draft',
243
+ resume_id TEXT REFERENCES documents(id) ON DELETE SET NULL,
244
+ cover_letter_id TEXT REFERENCES cover_letters(id) ON DELETE SET NULL,
245
+ applied_at TEXT,
246
+ notes TEXT,
247
+ created_at TEXT NOT NULL,
248
+ updated_at TEXT NOT NULL
249
+ );
250
+ INSERT INTO applications_new (id,job_id,status,resume_id,cover_letter_id,applied_at,notes,created_at,updated_at)
251
+ SELECT id,job_id,status,
252
+ CASE WHEN resume_id IN (SELECT id FROM documents) THEN resume_id ELSE NULL END,
253
+ CASE WHEN cover_letter_id IN (SELECT id FROM cover_letters) THEN cover_letter_id ELSE NULL END,
254
+ applied_at,notes,created_at,updated_at
255
+ FROM applications
256
+ WHERE job_id IN (SELECT id FROM jobs);
257
+ DROP TABLE applications;
258
+ ALTER TABLE applications_new RENAME TO applications;
259
+
260
+ -- interview_preps.job_id -> jobs(id) ON DELETE CASCADE
261
+ CREATE TABLE interview_preps_new (
262
+ id TEXT PRIMARY KEY,
263
+ job_id TEXT NOT NULL UNIQUE REFERENCES jobs(id) ON DELETE CASCADE,
264
+ questions TEXT NOT NULL DEFAULT '[]',
265
+ star_guides TEXT NOT NULL DEFAULT '[]',
266
+ self_introduction TEXT,
267
+ notes TEXT,
268
+ created_at TEXT NOT NULL,
269
+ updated_at TEXT NOT NULL
270
+ );
271
+ INSERT INTO interview_preps_new (id,job_id,questions,star_guides,self_introduction,notes,created_at,updated_at)
272
+ SELECT id,job_id,questions,star_guides,self_introduction,notes,created_at,updated_at
273
+ FROM interview_preps
274
+ WHERE job_id IN (SELECT id FROM jobs);
275
+ DROP TABLE interview_preps;
276
+ ALTER TABLE interview_preps_new RENAME TO interview_preps;
277
+
278
+ -- Recreate indexes dropped along with their tables.
279
+ CREATE INDEX IF NOT EXISTS idx_fit_job ON fit_analyses(job_id);
280
+ CREATE INDEX IF NOT EXISTS idx_clv_letter ON cover_letter_versions(cover_letter_id);
281
+ CREATE INDEX IF NOT EXISTS idx_cl_job ON cover_letters(job_id);
282
+
283
+ COMMIT;
284
+ PRAGMA foreign_keys=ON;
285
+ `,
286
+ ];
287
+
288
+ export function migrate(db: DatabaseSync): { from: number; to: number } {
289
+ db.exec(`CREATE TABLE IF NOT EXISTS _meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);`);
290
+ const row = db.prepare(`SELECT value FROM _meta WHERE key = 'schema_version'`).get() as
291
+ | { value: string }
292
+ | undefined;
293
+ const from = row ? Number(row.value) : 0;
294
+
295
+ for (let v = from; v < MIGRATIONS.length; v++) {
296
+ db.exec(MIGRATIONS[v]!);
297
+ }
298
+
299
+ const to = MIGRATIONS.length;
300
+ if (to !== from) {
301
+ db.prepare(
302
+ `INSERT INTO _meta (key, value) VALUES ('schema_version', ?)
303
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
304
+ ).run(String(to));
305
+ }
306
+ return { from, to };
307
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * @careermate/exporters — HTML helpers.
3
+ *
4
+ * `escapeHtml` and `toPrintableHtml` produce a clean, standalone, A4
5
+ * print-optimized HTML document. The print CSS is what powers our "PDF" export:
6
+ * the user opens the HTML in a browser and chooses "Print → Save as PDF", so we
7
+ * never ship a binary PDF library.
8
+ */
9
+ import { markdownToHtml } from './markdown.ts';
10
+
11
+ /** Escape the five HTML-significant characters. */
12
+ export function escapeHtml(input: string | null | undefined): string {
13
+ return (input ?? '')
14
+ .replace(/&/g, '&amp;')
15
+ .replace(/</g, '&lt;')
16
+ .replace(/>/g, '&gt;')
17
+ .replace(/"/g, '&quot;')
18
+ .replace(/'/g, '&#39;');
19
+ }
20
+
21
+ /**
22
+ * Wrap a markdown body in a complete, print-optimized HTML document.
23
+ *
24
+ * The body is ALWAYS rendered through {@link markdownToHtml}, which HTML-escapes
25
+ * its input before re-introducing only its own safe tags. Every exporter feeds
26
+ * this markdown it built itself, so there is no need to accept raw HTML — and
27
+ * accepting it would let user content (e.g. a cover-letter body containing
28
+ * `<img onerror=...>`) execute when the exported file is opened in a browser.
29
+ */
30
+ export function toPrintableHtml(title: string, bodyMarkdown: string): string {
31
+ const body = markdownToHtml(bodyMarkdown);
32
+ const safeTitle = escapeHtml(title);
33
+
34
+ return `<!doctype html>
35
+ <html lang="ko">
36
+ <head>
37
+ <meta charset="utf-8">
38
+ <meta name="viewport" content="width=device-width, initial-scale=1">
39
+ <title>${safeTitle}</title>
40
+ <style>
41
+ :root {
42
+ --ink: #1a1a1a;
43
+ --muted: #555;
44
+ --line: #d8d8d8;
45
+ --accent: #2b4a6f;
46
+ --maxw: 800px;
47
+ }
48
+ * { box-sizing: border-box; }
49
+ html, body {
50
+ margin: 0;
51
+ padding: 0;
52
+ color: var(--ink);
53
+ background: #f4f4f5;
54
+ font-family: "Pretendard", "Apple SD Gothic Neo", "Malgun Gothic",
55
+ -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
56
+ font-size: 14px;
57
+ line-height: 1.7;
58
+ -webkit-print-color-adjust: exact;
59
+ print-color-adjust: exact;
60
+ }
61
+ .page {
62
+ max-width: var(--maxw);
63
+ margin: 24px auto;
64
+ padding: 48px 56px;
65
+ background: #fff;
66
+ box-shadow: 0 1px 8px rgba(0, 0, 0, 0.08);
67
+ }
68
+ h1, h2, h3, h4, h5, h6 {
69
+ color: var(--accent);
70
+ line-height: 1.3;
71
+ margin: 1.4em 0 0.5em;
72
+ font-weight: 700;
73
+ }
74
+ h1 { font-size: 1.9em; margin-top: 0; border-bottom: 2px solid var(--accent); padding-bottom: 0.25em; }
75
+ h2 { font-size: 1.4em; border-bottom: 1px solid var(--line); padding-bottom: 0.2em; }
76
+ h3 { font-size: 1.15em; }
77
+ h4, h5, h6 { font-size: 1em; }
78
+ p { margin: 0.6em 0; }
79
+ ul, ol { margin: 0.5em 0 0.8em; padding-left: 1.4em; }
80
+ li { margin: 0.2em 0; }
81
+ a { color: var(--accent); text-decoration: none; }
82
+ a:hover { text-decoration: underline; }
83
+ strong { font-weight: 700; }
84
+ hr { border: none; border-top: 1px solid var(--line); margin: 1.5em 0; }
85
+ @page {
86
+ size: A4;
87
+ margin: 18mm 16mm;
88
+ }
89
+ @media print {
90
+ html, body { background: #fff; font-size: 12px; }
91
+ .page {
92
+ max-width: none;
93
+ margin: 0;
94
+ padding: 0;
95
+ box-shadow: none;
96
+ }
97
+ h1, h2, h3, h4, h5, h6 { page-break-after: avoid; }
98
+ ul, ol, p { page-break-inside: avoid; }
99
+ a { color: var(--ink); }
100
+ }
101
+ </style>
102
+ </head>
103
+ <body>
104
+ <main class="page">
105
+ ${body}
106
+ </main>
107
+ </body>
108
+ </html>
109
+ `;
110
+ }