diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml new file mode 100644 index 000000000..91e80c9dc --- /dev/null +++ b/.markdownlint-cli2.yaml @@ -0,0 +1,31 @@ +config: + default: true + MD007: false + MD009: false + MD010: false + MD012: false + MD013: false + MD001: false + MD022: false + MD024: false + MD025: false + MD028: false + MD031: false + MD032: false + MD033: false + MD034: false + MD036: false + MD040: false + MD041: false + MD046: false + MD060: false + +frontMatter: "^---[\\s\\S]*?---" +gitignore: true +globs: + - "**/*.{md,mdx}" +ignores: + - ".design-sync/**" + - "node_modules/**" + - ".turbo/**" + - "dist/**" diff --git a/apps/web/locales/af-ZA.po b/apps/web/locales/af-ZA.po index 92a280a1d..2edc6b375 100644 --- a/apps/web/locales/af-ZA.po +++ b/apps/web/locales/af-ZA.po @@ -2341,7 +2341,7 @@ msgstr "Japanees" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "Werk" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "Werksoekpyplyn" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "Werksgeleenthede" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "Laai tans KI-verskaffers. Probeer asseblief weer oor 'n oomblik." #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "Aansoeke laai…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Netwerk" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Nuwe aansoek" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Nuwe draad" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Nuwe draad" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Zoem uit" #: src/libs/locale.ts msgid "Zulu" msgstr "Zoeloe" - diff --git a/apps/web/locales/am-ET.po b/apps/web/locales/am-ET.po index 8357766a7..3799709d1 100644 --- a/apps/web/locales/am-ET.po +++ b/apps/web/locales/am-ET.po @@ -2341,7 +2341,7 @@ msgstr "ጃፓንኛ" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "ስራ" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "የሥራ ፍለጋ ሂደት" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "ስራዎች" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "የAI አቅራቢዎችን በመጫን ላይ። እባክዎ ከአ #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "ማመልከቻዎች በመጫን ላይ…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "ኔትዎርክ" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "አዲስ ማመልከቻ" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "አዲስ ክር" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "አዲስ ውይይት" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "አጉር" #: src/libs/locale.ts msgid "Zulu" msgstr "ዙሉ" - diff --git a/apps/web/locales/ar-SA.po b/apps/web/locales/ar-SA.po index 8e7d2f6a4..aa7e40872 100644 --- a/apps/web/locales/ar-SA.po +++ b/apps/web/locales/ar-SA.po @@ -2341,7 +2341,7 @@ msgstr "اليابانية" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "وظيفة" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "مسار البحث عن وظيفة" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "وظائف" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "جارٍ تحميل مزودي خدمات الذكاء الاصطناع #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "جارٍ تحميل طلبات التقديم…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "الشبكة" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "طلب تقديم جديد" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "موضوع جديد" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "موضوع جديد" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "تصغير" #: src/libs/locale.ts msgid "Zulu" msgstr "الزولو" - diff --git a/apps/web/locales/az-AZ.po b/apps/web/locales/az-AZ.po index 9e30032f0..05cf44855 100644 --- a/apps/web/locales/az-AZ.po +++ b/apps/web/locales/az-AZ.po @@ -2341,7 +2341,7 @@ msgstr "Yapon" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "İş" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "İş axtarışı prosesi" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "İşlər" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "Süni intellekt provayderləri yüklənir. Zəhmət olmasa, bir az sonra #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "Müraciətlər yüklənir…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Şəbəkə" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Yeni müraciət" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Yeni mövzu" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Yeni mövzu" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Uzaqlaşdır" #: src/libs/locale.ts msgid "Zulu" msgstr "Zulu" - diff --git a/apps/web/locales/bg-BG.po b/apps/web/locales/bg-BG.po index d37e6c6f3..e856a494c 100644 --- a/apps/web/locales/bg-BG.po +++ b/apps/web/locales/bg-BG.po @@ -2341,7 +2341,7 @@ msgstr "Японски" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "Работа" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "Поток за търсене на работа" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "Работни позиции" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "Зареждане на доставчици на ИИ. Моля, опи #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "Зареждане на кандидатури…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Мрежа" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Нова кандидатура" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Нова тема" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Нова нишка" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Намаляване" #: src/libs/locale.ts msgid "Zulu" msgstr "Зулуски" - diff --git a/apps/web/locales/bn-BD.po b/apps/web/locales/bn-BD.po index c0a3befa6..51aba2200 100644 --- a/apps/web/locales/bn-BD.po +++ b/apps/web/locales/bn-BD.po @@ -2341,7 +2341,7 @@ msgstr "জাপানি" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "চাকরি" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "চাকরি খোঁজার পাইপলাইন" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "চাকরিসমূহ" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "এআই প্রোভাইডার লোড করা হচ্ #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "আবেদন লোড হচ্ছে…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "নেটওয়ার্ক" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "নতুন আবেদন" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "নতুন থ্রেড" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "নতুন থ্রেড" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "জুম আউট" #: src/libs/locale.ts msgid "Zulu" msgstr "জুলু" - diff --git a/apps/web/locales/ca-ES.po b/apps/web/locales/ca-ES.po index 71017a8d4..9db703e70 100644 --- a/apps/web/locales/ca-ES.po +++ b/apps/web/locales/ca-ES.po @@ -2341,7 +2341,7 @@ msgstr "Japonès" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "Feina" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "Procés de cerca de feina" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "Feines" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "S'estan carregant els proveïdors d'IA. Si us plau, torneu-ho a provar d #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "S'estan carregant les candidatures…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Xarxa" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Nova candidatura" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Nou fil" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Fil nou" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Allunya" #: src/libs/locale.ts msgid "Zulu" msgstr "Zulú" - diff --git a/apps/web/locales/cs-CZ.po b/apps/web/locales/cs-CZ.po index ba3d2fb55..476daf764 100644 --- a/apps/web/locales/cs-CZ.po +++ b/apps/web/locales/cs-CZ.po @@ -2341,7 +2341,7 @@ msgstr "Japonština" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "Práce" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "Proces hledání práce" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "Pracovní místa" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "Načítání poskytovatelů umělé inteligence. Zkuste to prosím znovu #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "Načítají se žádosti…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Síť" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Nová žádost" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Nové vlákno" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Nové vlákno" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Oddálit" #: src/libs/locale.ts msgid "Zulu" msgstr "Zulu" - diff --git a/apps/web/locales/da-DK.po b/apps/web/locales/da-DK.po index 870a1681a..56ce58354 100644 --- a/apps/web/locales/da-DK.po +++ b/apps/web/locales/da-DK.po @@ -2341,7 +2341,7 @@ msgstr "Japansk" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "Job" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "Jobsøgningspipeline" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "Jobs" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "Indlæser AI-udbydere. Prøv igen om et øjeblik." #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "Indlæser ansøgninger…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Netværk" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Ny ansøgning" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Ny tråd" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Ny tråd" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Zoom ud" #: src/libs/locale.ts msgid "Zulu" msgstr "Zulu" - diff --git a/apps/web/locales/de-DE.po b/apps/web/locales/de-DE.po index 97a6c24a2..f9c7a8dc0 100644 --- a/apps/web/locales/de-DE.po +++ b/apps/web/locales/de-DE.po @@ -2341,7 +2341,7 @@ msgstr "Japanisch" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "Job" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "Bewerbungspipeline" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "Jobs" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "KI-Anbieter werden geladen. Bitte versuchen Sie es in einem Moment erneu #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "Bewerbungen werden geladen…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Netzwerk" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Neue Bewerbung" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Neuer Thread" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Neuer Thread" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Herauszoomen" #: src/libs/locale.ts msgid "Zulu" msgstr "Zulu" - diff --git a/apps/web/locales/el-GR.po b/apps/web/locales/el-GR.po index ccf2c5042..1e31945a0 100644 --- a/apps/web/locales/el-GR.po +++ b/apps/web/locales/el-GR.po @@ -2341,7 +2341,7 @@ msgstr "Ιαπωνικά" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "Θέση εργασίας" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "Ροή αναζήτησης εργασίας" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "Θέσεις εργασίας" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "Φόρτωση παρόχων τεχνητής νοημοσύνης. Δ #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "Φόρτωση αιτήσεων…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Δίκτυο" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Νέα αίτηση" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Νέο νήμα" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Νέο νήμα" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Σμίκρυνση" #: src/libs/locale.ts msgid "Zulu" msgstr "Ζουλού" - diff --git a/apps/web/locales/en-GB.po b/apps/web/locales/en-GB.po index 6745088af..f180b2b62 100644 --- a/apps/web/locales/en-GB.po +++ b/apps/web/locales/en-GB.po @@ -2341,7 +2341,7 @@ msgstr "Japanese" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "Job" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "Job search pipeline" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "Jobs" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "Loading AI providers. Please try again in a moment." #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "Loading applications…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Network" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "New Application" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "New thread" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "New Thread" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Zoom out" #: src/libs/locale.ts msgid "Zulu" msgstr "Zulu" - diff --git a/apps/web/locales/es-ES.po b/apps/web/locales/es-ES.po index a65fdea3d..b577ff29d 100644 --- a/apps/web/locales/es-ES.po +++ b/apps/web/locales/es-ES.po @@ -2789,7 +2789,7 @@ msgstr "Nuevo hilo" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Nuevo hilo" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Alejar" #: src/libs/locale.ts msgid "Zulu" msgstr "Zulú" - diff --git a/apps/web/locales/fa-IR.po b/apps/web/locales/fa-IR.po index d0234fddb..93d0dc81f 100644 --- a/apps/web/locales/fa-IR.po +++ b/apps/web/locales/fa-IR.po @@ -2341,7 +2341,7 @@ msgstr "ژاپنی" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "شغل" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "فرایند جستجوی شغل" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "شغل‌ها" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "در حال بارگذاری ارائه دهندگان هوش مصنوع #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "در حال بارگذاری درخواست‌ها…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "شبکه" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "درخواست جدید" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "تاپیک جدید" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "گفت‌وگوی جدید" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "کوچک‌نمایی" #: src/libs/locale.ts msgid "Zulu" msgstr "زولو" - diff --git a/apps/web/locales/fi-FI.po b/apps/web/locales/fi-FI.po index c54005874..c773b0b4c 100644 --- a/apps/web/locales/fi-FI.po +++ b/apps/web/locales/fi-FI.po @@ -2341,7 +2341,7 @@ msgstr "japani" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "Työpaikka" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "Työnhaun eteneminen" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "Työpaikat" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "Ladataan tekoälypalveluntarjoajia. Yritä uudelleen hetken kuluttua." #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "Ladataan hakemuksia…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Verkosto" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Uusi hakemus" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Uusi viestiketju" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Uusi ketju" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Loitonna" #: src/libs/locale.ts msgid "Zulu" msgstr "Zulu" - diff --git a/apps/web/locales/fr-FR.po b/apps/web/locales/fr-FR.po index 976513abe..c9eedafc6 100644 --- a/apps/web/locales/fr-FR.po +++ b/apps/web/locales/fr-FR.po @@ -2789,7 +2789,7 @@ msgstr "Nouveau sujet" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Nouveau fil" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Zoom arrière" #: src/libs/locale.ts msgid "Zulu" msgstr "Zoulou" - diff --git a/apps/web/locales/he-IL.po b/apps/web/locales/he-IL.po index cd43c6f61..6c7f0ccb3 100644 --- a/apps/web/locales/he-IL.po +++ b/apps/web/locales/he-IL.po @@ -2341,7 +2341,7 @@ msgstr "יפנית" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "משרה" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "תהליך חיפוש עבודה" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "משרות" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "טוען ספקי בינה מלאכותית. אנא נסה שוב בעו #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "טוען מועמדויות…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "רשת" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "מועמדות חדשה" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "שרשור חדש" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "שרשור חדש" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "התרחקות" #: src/libs/locale.ts msgid "Zulu" msgstr "זולו" - diff --git a/apps/web/locales/hi-IN.po b/apps/web/locales/hi-IN.po index 4ba4ebf25..bc9fc1364 100644 --- a/apps/web/locales/hi-IN.po +++ b/apps/web/locales/hi-IN.po @@ -2341,7 +2341,7 @@ msgstr "जापानी" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "नौकरी" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "नौकरी खोज पाइपलाइन" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "नौकरियाँ" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "एआई प्रदाताओं को लोड किया ज #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "आवेदन लोड हो रहे हैं…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "नेटवर्क" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "नया आवेदन" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "नया थ्रेड" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "नया थ्रेड" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "ज़ूम आउट" #: src/libs/locale.ts msgid "Zulu" msgstr "ज़ुलु" - diff --git a/apps/web/locales/hu-HU.po b/apps/web/locales/hu-HU.po index 4d8b8efab..f56137978 100644 --- a/apps/web/locales/hu-HU.po +++ b/apps/web/locales/hu-HU.po @@ -2341,7 +2341,7 @@ msgstr "japán" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "Állás" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "Álláskeresési folyamat" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "Állások" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "MI-szolgáltatók betöltése folyamatban. Kérjük, próbálja újra eg #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "Jelentkezések betöltése…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Hálózat" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Új jelentkezés" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Új téma" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Új szál" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Kicsinyítés" #: src/libs/locale.ts msgid "Zulu" msgstr "Zulu" - diff --git a/apps/web/locales/id-ID.po b/apps/web/locales/id-ID.po index 623921bc1..c51f0c5f8 100644 --- a/apps/web/locales/id-ID.po +++ b/apps/web/locales/id-ID.po @@ -2341,7 +2341,7 @@ msgstr "Jepang" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "Pekerjaan" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "Alur pencarian kerja" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "Pekerjaan" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "Sedang memuat penyedia AI. Silakan coba lagi sebentar lagi." #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "Memuat lamaran…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Jaringan" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Lamaran baru" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Utas baru" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Utas baru" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Perkecil" #: src/libs/locale.ts msgid "Zulu" msgstr "Zulu" - diff --git a/apps/web/locales/it-IT.po b/apps/web/locales/it-IT.po index 369a429c1..d1a5ebbf2 100644 --- a/apps/web/locales/it-IT.po +++ b/apps/web/locales/it-IT.po @@ -2341,7 +2341,7 @@ msgstr "Giapponese" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "Lavoro" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "Pipeline di ricerca lavoro" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "Lavori" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "Caricamento dei provider di intelligenza artificiale. Riprova tra un att #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "Caricamento candidature…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Rete" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Nuova candidatura" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Nuovo thread" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Nuovo thread" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Rimpicciolisci" #: src/libs/locale.ts msgid "Zulu" msgstr "Zulu" - diff --git a/apps/web/locales/ja-JP.po b/apps/web/locales/ja-JP.po index a3c2774a9..ef328933c 100644 --- a/apps/web/locales/ja-JP.po +++ b/apps/web/locales/ja-JP.po @@ -2341,7 +2341,7 @@ msgstr "日本語" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "求人" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "求人応募パイプライン" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "求人" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "AIプロバイダーを読み込んでいます。しばらくしてか #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "応募を読み込み中…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "ネットワーク" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "新しい応募" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "新しいスレッド" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "新しいスレッド" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "ズームアウト" #: src/libs/locale.ts msgid "Zulu" msgstr "ズールー語" - diff --git a/apps/web/locales/km-KH.po b/apps/web/locales/km-KH.po index 43142bdb3..7c04682e1 100644 --- a/apps/web/locales/km-KH.po +++ b/apps/web/locales/km-KH.po @@ -2341,7 +2341,7 @@ msgstr "Japanese" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "ការងារ" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "ដំណើរការស្វែងរកការងារ" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "ការងារ" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "កំពុងផ្ទុកអ្នកផ្តល់សេវា AI #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "កំពុងផ្ទុកពាក្យសុំ…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "បណ្ដាញ" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "ពាក្យសុំថ្មី" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "ខ្សែស្រឡាយថ្មី" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "ខ្សែសន្ទនាថ្មី" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "បង្រួម" #: src/libs/locale.ts msgid "Zulu" msgstr "Zulu" - diff --git a/apps/web/locales/kn-IN.po b/apps/web/locales/kn-IN.po index ced014c08..31eeacfc8 100644 --- a/apps/web/locales/kn-IN.po +++ b/apps/web/locales/kn-IN.po @@ -2341,7 +2341,7 @@ msgstr "ಜಾಪನೀಸ್" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "ಉದ್ಯೋಗ" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "ಉದ್ಯೋಗ ಹುಡುಕಾಟ ಪೈಪ್‌ಲೈನ್" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "ಉದ್ಯೋಗಗಳು" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "AI ಪೂರೈಕೆದಾರರನ್ನು ಲೋಡ್ ಮಾಡಲ #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "ಅರ್ಜಿಗಳನ್ನು ಲೋಡ್ ಮಾಡಲಾಗುತ್ತಿದೆ…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "ನೆಟ್ವರ್ಕ್" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "ಹೊಸ ಅರ್ಜಿ" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "ಹೊಸ ಥ್ರೆಡ್" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "ಹೊಸ ಥ್ರೆಡ್" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "ಗಾತ್ರ ಕುಗ್ಗಿಸಿ" #: src/libs/locale.ts msgid "Zulu" msgstr "ಜೂಲೂ" - diff --git a/apps/web/locales/ko-KR.po b/apps/web/locales/ko-KR.po index f512900fa..8d65c9a95 100644 --- a/apps/web/locales/ko-KR.po +++ b/apps/web/locales/ko-KR.po @@ -2341,7 +2341,7 @@ msgstr "일본어" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "채용 공고" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "구직 파이프라인" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "채용 공고" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "AI 제공업체를 불러오는 중입니다. 잠시 후 다시 시도 #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "지원서를 불러오는 중…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "네트워크" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "새 지원서" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "새 스레드" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "새 스레드" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "축소" #: src/libs/locale.ts msgid "Zulu" msgstr "줄루어" - diff --git a/apps/web/locales/lt-LT.po b/apps/web/locales/lt-LT.po index 891026a42..0f4e5fc7d 100644 --- a/apps/web/locales/lt-LT.po +++ b/apps/web/locales/lt-LT.po @@ -2341,7 +2341,7 @@ msgstr "Japonų" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "Darbas" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "Darbo paieškos eiga" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "Darbai" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "Įkeliami dirbtinio intelekto teikėjai. Bandykite dar kartą po akimirk #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "Įkeliamos paraiškos…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Tinklas" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Nauja paraiška" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Nauja tema" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Nauja gija" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Tolinti" #: src/libs/locale.ts msgid "Zulu" msgstr "Zulų" - diff --git a/apps/web/locales/lv-LV.po b/apps/web/locales/lv-LV.po index 04384d7c5..a798f5d67 100644 --- a/apps/web/locales/lv-LV.po +++ b/apps/web/locales/lv-LV.po @@ -2341,7 +2341,7 @@ msgstr "Japāņu" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "Darbs" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "Darba meklēšanas process" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "Darbi" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "Notiek mākslīgā intelekta pakalpojumu sniedzēju ielāde. Lūdzu, mē #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "Notiek pieteikumu ielāde…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Tīkls" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Jauns pieteikums" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Jauna tēma" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Jauns pavediens" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Tālināt" #: src/libs/locale.ts msgid "Zulu" msgstr "Zulu" - diff --git a/apps/web/locales/ml-IN.po b/apps/web/locales/ml-IN.po index 874d690a8..62403273c 100644 --- a/apps/web/locales/ml-IN.po +++ b/apps/web/locales/ml-IN.po @@ -2341,7 +2341,7 @@ msgstr "ജാപ്പനീസ്" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "ജോലി" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "ജോലി തിരച്ചിൽ പൈപ്പ്‌ലൈൻ" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "ജോലികൾ" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "AI ദാതാക്കളെ ലോഡ് ചെയ്യുന്ന #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "അപേക്ഷകൾ ലോഡ് ചെയ്യുന്നു…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "നെറ്റ്‌വർക്ക്" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "പുതിയ അപേക്ഷ" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "പുതിയ ത്രെഡ്" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "പുതിയ ത്രെഡ്" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "സൂം ഔട്ട് ചെയ്യുക" #: src/libs/locale.ts msgid "Zulu" msgstr "സൂളു" - diff --git a/apps/web/locales/mr-IN.po b/apps/web/locales/mr-IN.po index 40a33692e..270115bc8 100644 --- a/apps/web/locales/mr-IN.po +++ b/apps/web/locales/mr-IN.po @@ -2341,7 +2341,7 @@ msgstr "जपानी" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "नोकरी" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "नोकरी शोध पाइपलाइन" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "नोकऱ्या" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "एआय प्रोव्हायडर्स लोड होत #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "अर्ज लोड होत आहेत…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "नेटवर्क" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "नवीन अर्ज" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "नवीन धागा" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "नवीन थ्रेड" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "बाहेर झूम करा" #: src/libs/locale.ts msgid "Zulu" msgstr "झुलू" - diff --git a/apps/web/locales/ms-MY.po b/apps/web/locales/ms-MY.po index 5bbaf5e91..5254f16ae 100644 --- a/apps/web/locales/ms-MY.po +++ b/apps/web/locales/ms-MY.po @@ -2341,7 +2341,7 @@ msgstr "Jepun" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "Pekerjaan" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "Saluran carian kerja" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "Pekerjaan" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "Memuatkan penyedia AI. Sila cuba lagi sebentar lagi." #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "Memuatkan permohonan…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Rangkaian" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Permohonan baharu" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Benang baharu" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Utas baharu" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Zum keluar" #: src/libs/locale.ts msgid "Zulu" msgstr "Zulu" - diff --git a/apps/web/locales/ne-NP.po b/apps/web/locales/ne-NP.po index f89da55c4..3bde54dcd 100644 --- a/apps/web/locales/ne-NP.po +++ b/apps/web/locales/ne-NP.po @@ -2341,7 +2341,7 @@ msgstr "जापानी" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "जागिर" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "रोजगार खोज पाइपलाइन" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "जागिरहरू" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "AI प्रदायकहरू लोड गर्दै। कृप #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "आवेदनहरू लोड हुँदैछन्…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "नेटवर्क" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "नयाँ आवेदन" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "नयाँ थ्रेड" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "नयाँ थ्रेड" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "जूम आउट" #: src/libs/locale.ts msgid "Zulu" msgstr "जुलु" - diff --git a/apps/web/locales/nl-NL.po b/apps/web/locales/nl-NL.po index c3c747ae1..0498dfe97 100644 --- a/apps/web/locales/nl-NL.po +++ b/apps/web/locales/nl-NL.po @@ -2341,7 +2341,7 @@ msgstr "Japans" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "Baan" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "Sollicitatiepijplijn" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "Banen" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "AI-providers worden geladen. Probeer het over een moment opnieuw." #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "Sollicitaties laden…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Netwerk" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Nieuwe sollicitatie" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Nieuw topic" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Nieuwe thread" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Uitzoomen" #: src/libs/locale.ts msgid "Zulu" msgstr "Zulu" - diff --git a/apps/web/locales/no-NO.po b/apps/web/locales/no-NO.po index 766fe01c9..019faeb0f 100644 --- a/apps/web/locales/no-NO.po +++ b/apps/web/locales/no-NO.po @@ -2341,7 +2341,7 @@ msgstr "Japansk" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "Jobb" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "Jobbsøkpipeline" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "Jobber" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "Laster inn AI-leverandører. Prøv igjen om et øyeblikk." #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "Laster søknader…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Nettverk" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Ny søknad" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Ny tråd" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Ny tråd" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Zoom ut" #: src/libs/locale.ts msgid "Zulu" msgstr "Zulu" - diff --git a/apps/web/locales/or-IN.po b/apps/web/locales/or-IN.po index afe2af8b7..4761988fe 100644 --- a/apps/web/locales/or-IN.po +++ b/apps/web/locales/or-IN.po @@ -2341,7 +2341,7 @@ msgstr "ଜାପାନୀ" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "ଚାକିରି" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "ଚାକିରି ଖୋଜ ପାଇପଲାଇନ୍" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "ଚାକିରିଗୁଡ଼ିକ" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "AI ପ୍ରଦାନକାରୀଙ୍କୁ ଲୋଡ୍ କରାଯ #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "ଆବେଦନଗୁଡ଼ିକ ଲୋଡ୍ ହେଉଛି…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "ନେଟୱର୍କ" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "ନୂତନ ଆବେଦନ" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "ନୂଆ ଥ୍ରେଡ୍‌" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "ନୂତନ ଥ୍ରେଡ୍" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "ଜୁମ୍ ଆଉଟ୍ କରନ୍ତୁ" #: src/libs/locale.ts msgid "Zulu" msgstr "ଜୁଲୁ" - diff --git a/apps/web/locales/pl-PL.po b/apps/web/locales/pl-PL.po index ae81b5dce..1904af34b 100644 --- a/apps/web/locales/pl-PL.po +++ b/apps/web/locales/pl-PL.po @@ -2341,7 +2341,7 @@ msgstr "Japoński" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "Praca" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "Proces szukania pracy" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "Oferty pracy" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "Ładowanie dostawców sztucznej inteligencji. Spróbuj ponownie za chwil #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "Ładowanie aplikacji…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Sieć" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Nowa aplikacja" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Nowy wątek" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Nowy wątek" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Pomniejsz" #: src/libs/locale.ts msgid "Zulu" msgstr "Zulu" - diff --git a/apps/web/locales/pt-BR.po b/apps/web/locales/pt-BR.po index 7004d320a..c0ae93c01 100644 --- a/apps/web/locales/pt-BR.po +++ b/apps/web/locales/pt-BR.po @@ -2341,7 +2341,7 @@ msgstr "Japonês" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "Vaga" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "Pipeline de busca de emprego" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "Vagas" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "Carregando provedores de IA. Tente novamente em instantes." #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "Carregando candidaturas…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Rede" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Nova candidatura" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Novo tópico" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Novo tópico" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Diminuir zoom" #: src/libs/locale.ts msgid "Zulu" msgstr "Zulu" - diff --git a/apps/web/locales/pt-PT.po b/apps/web/locales/pt-PT.po index 719856d3d..1149e7957 100644 --- a/apps/web/locales/pt-PT.po +++ b/apps/web/locales/pt-PT.po @@ -2341,7 +2341,7 @@ msgstr "Japonês" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "Vaga" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "Fluxo de procura de emprego" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "Vagas" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "Carregando fornecedores de IA. Tente novamente dentro de instantes." #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "A carregar candidaturas…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Rede social" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Nova candidatura" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Novo tópico" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Novo tópico" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Afastar" #: src/libs/locale.ts msgid "Zulu" msgstr "Zulu" - diff --git a/apps/web/locales/ro-RO.po b/apps/web/locales/ro-RO.po index e0569adb2..fd7efd567 100644 --- a/apps/web/locales/ro-RO.po +++ b/apps/web/locales/ro-RO.po @@ -2789,7 +2789,7 @@ msgstr "Fir nou" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Fir nou" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Micșorează" #: src/libs/locale.ts msgid "Zulu" msgstr "Zulu" - diff --git a/apps/web/locales/ru-RU.po b/apps/web/locales/ru-RU.po index 9a41b8491..ec09b4913 100644 --- a/apps/web/locales/ru-RU.po +++ b/apps/web/locales/ru-RU.po @@ -2341,7 +2341,7 @@ msgstr "Японский" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "Вакансия" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "Воронка поиска работы" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "Вакансии" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "Загрузка поставщиков ИИ. Пожалуйста, по #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "Загрузка откликов…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Сеть" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Новый отклик" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Новая тема" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Новая тема" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Уменьшить" #: src/libs/locale.ts msgid "Zulu" msgstr "Зулу" - diff --git a/apps/web/locales/sk-SK.po b/apps/web/locales/sk-SK.po index 0eb4c0373..c8120938a 100644 --- a/apps/web/locales/sk-SK.po +++ b/apps/web/locales/sk-SK.po @@ -2341,7 +2341,7 @@ msgstr "Japončina" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "Práca" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "Proces hľadania práce" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "Pracovné miesta" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "Načítavajú sa poskytovatelia umelej inteligencie. Skúste to znova o #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "Načítavajú sa žiadosti…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Sieť" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Nová žiadosť" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Nové vlákno" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Nové vlákno" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Oddialiť" #: src/libs/locale.ts msgid "Zulu" msgstr "Zulu" - diff --git a/apps/web/locales/sl-SI.po b/apps/web/locales/sl-SI.po index 53dbb74b9..06e778df7 100644 --- a/apps/web/locales/sl-SI.po +++ b/apps/web/locales/sl-SI.po @@ -2341,7 +2341,7 @@ msgstr "Japonščina" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "Zaposlitev" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "Potek iskanja zaposlitve" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "Zaposlitve" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "Nalaganje ponudnikov umetne inteligence. Poskusite znova čez trenutek." #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "Nalaganje prijav…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Omrežje" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Nova prijava" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Nova nit" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Nova nit" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Oddalji" #: src/libs/locale.ts msgid "Zulu" msgstr "Zuluščina" - diff --git a/apps/web/locales/sq-AL.po b/apps/web/locales/sq-AL.po index c90905606..e452d1fea 100644 --- a/apps/web/locales/sq-AL.po +++ b/apps/web/locales/sq-AL.po @@ -2341,7 +2341,7 @@ msgstr "Japonisht" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "Punë" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "Procesi i kërkimit të punës" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "Punë" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "Duke ngarkuar ofruesit e inteligjencës artificiale. Ju lutemi provoni p #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "Duke ngarkuar aplikimet…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Rrjeti" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Aplikim i ri" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Fije e re" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Fill i ri" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Zvogëlo" #: src/libs/locale.ts msgid "Zulu" msgstr "Zulu" - diff --git a/apps/web/locales/sr-SP.po b/apps/web/locales/sr-SP.po index 4f38d23ea..688944877 100644 --- a/apps/web/locales/sr-SP.po +++ b/apps/web/locales/sr-SP.po @@ -2341,7 +2341,7 @@ msgstr "Јапански" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "Посао" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "Процес тражења посла" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "Послови" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "Учитавање добављача вештачке интелиге #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "Учитавање пријава…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Мрежа" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Нова пријава" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Нова тема" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Нова тема" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Умањи" #: src/libs/locale.ts msgid "Zulu" msgstr "Зулу" - diff --git a/apps/web/locales/sv-SE.po b/apps/web/locales/sv-SE.po index d362225bc..894290347 100644 --- a/apps/web/locales/sv-SE.po +++ b/apps/web/locales/sv-SE.po @@ -2341,7 +2341,7 @@ msgstr "Japanska" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "Jobb" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "Jobbsökningsflöde" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "Jobb" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "Laddar AI-leverantörer. Försök igen om en stund." #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "Läser in ansökningar…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Nätverk" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Ny ansökan" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Ny tråd" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Ny tråd" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Zooma ut" #: src/libs/locale.ts msgid "Zulu" msgstr "Zulu" - diff --git a/apps/web/locales/ta-IN.po b/apps/web/locales/ta-IN.po index cbe280bc9..48667dcf6 100644 --- a/apps/web/locales/ta-IN.po +++ b/apps/web/locales/ta-IN.po @@ -2341,7 +2341,7 @@ msgstr "ஜப்பானியம்" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "வேலை" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "வேலை தேடல் நிலைவரிசை" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "வேலைகள்" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "செயற்கை நுண்ணறிவு வழங்குந #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "விண்ணப்பங்கள் ஏற்றப்படுகின்றன…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "நெட்வொர்க்" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "புதிய விண்ணப்பம்" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "புதிய இழை" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "புதிய இழை" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "சிறிதாக்கு" #: src/libs/locale.ts msgid "Zulu" msgstr "ஜூலு" - diff --git a/apps/web/locales/te-IN.po b/apps/web/locales/te-IN.po index 71d664c38..6540aa83a 100644 --- a/apps/web/locales/te-IN.po +++ b/apps/web/locales/te-IN.po @@ -2341,7 +2341,7 @@ msgstr "జపానీస్" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "ఉద్యోగం" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "ఉద్యోగ శోధన పైప్‌లైన్" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "ఉద్యోగాలు" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "AI ప్రొవైడర్లు లోడ్ అవుతున్ #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "దరఖాస్తులు లోడ్ అవుతున్నాయి…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "నెట్‌వర్క్" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "కొత్త దరఖాస్తు" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "కొత్త థ్రెడ్" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "కొత్త థ్రెడ్" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "జూమ్ అవుట్ చేయండి" #: src/libs/locale.ts msgid "Zulu" msgstr "జులు" - diff --git a/apps/web/locales/th-TH.po b/apps/web/locales/th-TH.po index d4eb7c0d5..dd87741e0 100644 --- a/apps/web/locales/th-TH.po +++ b/apps/web/locales/th-TH.po @@ -2341,7 +2341,7 @@ msgstr "ญี่ปุ่น" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "งาน" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "ไปป์ไลน์ค้นหางาน" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "งาน" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "กำลังโหลดผู้ให้บริการ AI โ #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "กำลังโหลดใบสมัคร…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "ชื่อเว็บไซต์" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "ใบสมัครใหม่" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "กระทู้ใหม่" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "เธรดใหม่" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "ซูมออก" #: src/libs/locale.ts msgid "Zulu" msgstr "ซูลู" - diff --git a/apps/web/locales/tr-TR.po b/apps/web/locales/tr-TR.po index c83562916..9b4c1a86c 100644 --- a/apps/web/locales/tr-TR.po +++ b/apps/web/locales/tr-TR.po @@ -2341,7 +2341,7 @@ msgstr "Japonca" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "İş" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "İş arama hattı" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "İşler" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "Yapay zeka sağlayıcıları yükleniyor. Lütfen bir süre sonra tekrar #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "Başvurular yükleniyor…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Sosyal Ağ" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Yeni başvuru" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Yeni konu" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Yeni konu" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Uzaklaştır" #: src/libs/locale.ts msgid "Zulu" msgstr "Zulu" - diff --git a/apps/web/locales/uk-UA.po b/apps/web/locales/uk-UA.po index 35bbdb2b5..11fc154d3 100644 --- a/apps/web/locales/uk-UA.po +++ b/apps/web/locales/uk-UA.po @@ -2341,7 +2341,7 @@ msgstr "Японська" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "Вакансія" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "Воронка пошуку роботи" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "Вакансії" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "Завантаження постачальників ШІ. Будь л #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "Завантаження заявок…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Мережа" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Нова заявка" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Нова тема" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Нова тема" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Зменшити" #: src/libs/locale.ts msgid "Zulu" msgstr "Зулу" - diff --git a/apps/web/locales/uz-UZ.po b/apps/web/locales/uz-UZ.po index 9bacfdf84..feb974b94 100644 --- a/apps/web/locales/uz-UZ.po +++ b/apps/web/locales/uz-UZ.po @@ -2341,7 +2341,7 @@ msgstr "Yapon tili" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "Ish" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "Ish qidirish jarayoni" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "Ishlar" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "Sun'iy intellekt provayderlari yuklanmoqda. Iltimos, bir zumda qayta uri #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "Arizalar yuklanmoqda…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Tarmoq" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Yangi ariza" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Yangi mavzu" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Yangi mavzu" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Kichraytirish" #: src/libs/locale.ts msgid "Zulu" msgstr "Zulu" - diff --git a/apps/web/locales/vi-VN.po b/apps/web/locales/vi-VN.po index d39965f19..3e65c9487 100644 --- a/apps/web/locales/vi-VN.po +++ b/apps/web/locales/vi-VN.po @@ -2341,7 +2341,7 @@ msgstr "Tiếng Nhật" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "Công việc" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "Quy trình tìm việc" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "Công việc" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "Đang tải các nhà cung cấp AI. Vui lòng thử lại sau một lá #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "Đang tải hồ sơ ứng tuyển…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "Mạng" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "Hồ sơ ứng tuyển mới" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "Chủ đề mới" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "Chủ đề mới" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "Thu nhỏ" #: src/libs/locale.ts msgid "Zulu" msgstr "Zulu" - diff --git a/apps/web/locales/zh-CN.po b/apps/web/locales/zh-CN.po index 4f6afab7f..88f097d8f 100644 --- a/apps/web/locales/zh-CN.po +++ b/apps/web/locales/zh-CN.po @@ -2341,7 +2341,7 @@ msgstr "日语" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "职位" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "求职流程" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "职位" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "正在加载人工智能提供商。请稍后再试。" #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "正在加载申请…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "网络" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "新申请" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "新帖" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "新帖" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "缩小" #: src/libs/locale.ts msgid "Zulu" msgstr "祖鲁语" - diff --git a/apps/web/locales/zh-TW.po b/apps/web/locales/zh-TW.po index 780719a30..7fd9c3cb4 100644 --- a/apps/web/locales/zh-TW.po +++ b/apps/web/locales/zh-TW.po @@ -2341,7 +2341,7 @@ msgstr "日文" #: src/features/command-palette/pages/navigation.tsx msgid "Job" -msgstr "" +msgstr "職缺" #: src/features/applications/components/application-form-sheet.tsx msgid "Job description" @@ -2357,7 +2357,7 @@ msgstr "求職流程" #: src/features/command-palette/pages/navigation.tsx msgid "Jobs" -msgstr "" +msgstr "職缺" #. Example full name placeholder on profile settings form #. Example full name placeholder on registration form @@ -2585,7 +2585,7 @@ msgstr "正在加載人工智慧提供者。請稍後再試。" #: src/features/command-palette/pages/resumes.tsx msgid "Loading applications…" -msgstr "" +msgstr "正在載入應徵紀錄…" #: src/features/settings/integrations/components/ai-section.tsx #: src/routes/agent/-components/new-thread-setup.tsx @@ -2752,7 +2752,7 @@ msgstr "網路" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Application" -msgstr "" +msgstr "新增應徵紀錄" #: src/routes/_home/-sections/features.tsx msgid "New features are constantly being added and improved, so be sure to check back often." @@ -2789,7 +2789,7 @@ msgstr "新帖" #: src/features/command-palette/pages/navigation.tsx #: src/features/command-palette/pages/resumes.tsx msgid "New Thread" -msgstr "" +msgstr "新帖" #: src/routes/_home/-sections/features.tsx msgid "No Advertising, No Tracking" @@ -4984,4 +4984,3 @@ msgstr "縮小" #: src/libs/locale.ts msgid "Zulu" msgstr "祖魯語" - diff --git a/docs/changelog/index.mdx b/docs/changelog/index.mdx index e0968fb6a..cb88d9587 100644 --- a/docs/changelog/index.mdx +++ b/docs/changelog/index.mdx @@ -4,6 +4,35 @@ description: "Release notes for Reactive Resume covering new features, resume bu rss: true --- + +## Highlights + +- **Application Tracker REST and MCP parity.** The job application pipeline is now available outside the web app through authenticated REST endpoints and MCP tools, so agents can create records, import rows, move stages, add notes, manage follow-ups, attach sent PDFs, and inspect pipeline health. [2dad27da5](https://github.com/amruthpillai/reactive-resume/commit/2dad27da5), [bf2dc5bbd](https://github.com/amruthpillai/reactive-resume/commit/bf2dc5bbd) +- **Agent-only application tracking.** MCP clients can now operate the Application Tracker end to end, including Application Copilot actions for job auto-fill, match scoring, tailored resume copies, cover letters, and recruiter follow-ups. [bf2dc5bbd](https://github.com/amruthpillai/reactive-resume/commit/bf2dc5bbd), [93abb1102](https://github.com/amruthpillai/reactive-resume/commit/93abb1102) +- **New application MCP guide.** The docs now include a dedicated guide with practical prompts for using Reactive Resume MCP to manage applications from an AI client. + +## Application Tracker API + +- Added public REST coverage for listing, reading, creating, updating, importing, bulk-updating, bulk-deleting, tagging, and summarizing job applications. [2dad27da5](https://github.com/amruthpillai/reactive-resume/commit/2dad27da5) +- Added authenticated document endpoints for attaching and removing the resume or cover-letter PDF sent with an application. [2dad27da5](https://github.com/amruthpillai/reactive-resume/commit/2dad27da5), [fb3f40244](https://github.com/amruthpillai/reactive-resume/commit/fb3f40244) +- Added REST endpoints for Application Copilot actions: job posting auto-fill, resume match scoring, tailored resume creation, and message drafting. [2dad27da5](https://github.com/amruthpillai/reactive-resume/commit/2dad27da5) +- Updated the OpenAPI reference so application endpoints, multipart document uploads, statistics, and tags are visible in the docs. [fb3f40244](https://github.com/amruthpillai/reactive-resume/commit/fb3f40244) + +## MCP & Agent Workflows + +- Added MCP tools for every Application Tracker workflow available in the app, including create, update, note, import, bulk action, archive, delete, document attachment, document removal, tag, statistics, and Application Copilot tools. [bf2dc5bbd](https://github.com/amruthpillai/reactive-resume/commit/bf2dc5bbd) +- Improved MCP tool metadata so clients can distinguish safe reads, mutating actions, destructive actions, and AI-powered workflows more reliably. [93abb1102](https://github.com/amruthpillai/reactive-resume/commit/93abb1102) +- Updated the resume-builder skill instructions so agents are told that Reactive Resume MCP can also manage job applications, not only resumes. [ac8734be3](https://github.com/amruthpillai/reactive-resume/commit/ac8734be3) + +## Documentation & Reliability + +- Added a parity design note for the Application Tracker REST and MCP surfaces. [3868429ce](https://github.com/amruthpillai/reactive-resume/commit/3868429ce) +- Documented application MCP automation in the MCP guide, including available tools, prompt examples, and troubleshooting for PDF attachments. [ed83a3f5e](https://github.com/amruthpillai/reactive-resume/commit/ed83a3f5e) +- Preserved shared application documents when one application removes or replaces a file also referenced by another application. [3c9c5c8c2](https://github.com/amruthpillai/reactive-resume/commit/3c9c5c8c2) + +**Full Changelog**: [v5.2.1...v5.2.2](https://github.com/amruthpillai/reactive-resume/compare/v5.2.1...v5.2.2) + + ## Highlights diff --git a/docs/docs.json b/docs/docs.json index 5f46b205b..4e268583a 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -53,8 +53,6 @@ "pages": [ "guides/creating-your-first-resume", "guides/managing-resumes-from-the-dashboard", - "guides/tracking-job-applications", - "guides/importing-applications-from-csv", "guides/importing-resumes", "guides/choosing-a-template", "guides/selecting-page-format", @@ -72,6 +70,14 @@ "guides/sharing-your-resume-publicly" ] }, + { + "group": "Application Tracker", + "pages": [ + "guides/tracking-job-applications", + "guides/importing-applications-from-csv", + "guides/managing-applications-with-mcp" + ] + }, { "group": "Security", "pages": ["guides/setting-up-two-factor-authentication", "guides/setting-up-passkeys"] diff --git a/docs/guides/importing-applications-from-csv.mdx b/docs/guides/importing-applications-from-csv.mdx index 8b975aa6e..884d73753 100644 --- a/docs/guides/importing-applications-from-csv.mdx +++ b/docs/guides/importing-applications-from-csv.mdx @@ -91,6 +91,10 @@ Reactive Resume imports up to 500 applications at a time. If your CSV has more t Import first, then use the table view to select multiple applications and apply tags, move stages, archive rows, or delete rows in bulk. + + MCP clients can also import application rows directly with `import_applications`. See [Managing applications with MCP](/guides/managing-applications-with-mcp) for agent prompt examples. + + ## Troubleshooting ### Some rows were skipped diff --git a/docs/guides/managing-applications-with-mcp.mdx b/docs/guides/managing-applications-with-mcp.mdx new file mode 100644 index 000000000..7e0bcb06c --- /dev/null +++ b/docs/guides/managing-applications-with-mcp.mdx @@ -0,0 +1,286 @@ +--- +title: "Managing applications with MCP" +description: "Use a connected MCP client to track job applications, attach sent PDFs, run Application Copilot, and manage your pipeline with natural-language prompts." +--- + +Use this guide when you want an AI client to manage the **Application Tracker** for you. After the Reactive Resume MCP server is connected, the agent can use tools for the same application workflows available in the app: listing applications, creating records, importing rows, updating stages, adding notes, managing follow-ups, attaching documents, and running Application Copilot. + +## Prerequisites + + + + Follow [Using the MCP Server](/guides/using-the-mcp-server) to connect your MCP client with OAuth or an API key. + + + + Ask your client to list the Reactive Resume tools. You should see tools such as `list_applications`, `create_application`, `update_application`, `attach_application_document`, and `draft_application_message`. + + + + Tell your agent to ask before deleting applications, bulk-updating many records, or replacing attached documents. + + + + + Application Copilot tools require the same AI provider setup used by the app. Match scoring and resume tailoring work best when the application has a linked Reactive Resume and a job description. + + +## Start with a pipeline review + +Begin by asking the agent to inspect your current pipeline. This gives the agent valid application IDs and avoids duplicate records. + +```text +List my active applications grouped by stage. Include company, role, tags, follow-up date, linked resume name, and whether a resume or cover-letter PDF is attached. +``` + +Useful review prompts: + +```text +Show me applications that need follow-up this week. +``` + +```text +Find applications tagged remote that are still in saved or applied stage. +``` + +```text +Summarize my pipeline stats by stage and source, then point out stale applications. +``` + +```text +List archived applications from the last 90 days. +``` + +## Create applications + +Use `create_application` when you already know the role details. + +```text +Create an application for Senior Product Engineer at Acme. Stage: saved. Location: Berlin or remote. Source: LinkedIn. Tags: remote, typescript, senior. Add a note that I want to tailor my platform resume before applying. +``` + +If you have a job posting, ask the agent to extract details first. + +```text +Use the Reactive Resume application auto-fill tool on this job posting URL, then create a saved application from the extracted company, role, location, salary, and job description. Tag it with remote and backend. +``` + +If the posting is private, paste the description into your prompt: + +```text +Create an application from this pasted job description. Use auto-fill if available, keep the stage as saved, and tag it with ai, platform, and high-priority. + +[Paste the job description here] +``` + +## Import applications + +Use `import_applications` when you already have spreadsheet rows. The Application Tracker accepts up to 500 imported rows at a time. + +```text +Import these application rows into Reactive Resume. Normalize the stages to saved, applied, screening, interview, offer, or rejected. Skip rows that do not have both a company and role, and tell me what was skipped. + +Company,Role,Stage,Location,Source,Tags +Acme,Frontend Engineer,applied,Remote,LinkedIn,remote;react +Globex,Staff Engineer,interview,Berlin,Referral,staff;platform +``` + +```text +I am pasting rows from my spreadsheet. Import them, tag every imported application with migrated-2026, and leave archived as false. +``` + +```text +Import these rejected applications and mark them as archived after import. +``` + +## Update stages and notes + +Use `update_application` for structured changes and `add_application_note` when you want an activity timeline entry without changing other fields. + +```text +Move my Acme Senior Product Engineer application to interview and add a note: Recruiter screen scheduled for July 12 at 10:00. +``` + +```text +Add a note to the Globex application: Submitted take-home assignment and waiting for review. +``` + +```text +Set a follow-up date for the Stripe application to next Monday, with the note: Ask whether they need more portfolio examples. +``` + +```text +Update the contacts on the Acme application. Recruiter: Priya Shah, priya@example.com. Hiring manager: Jordan Lee, LinkedIn URL https://www.linkedin.com/in/example. +``` + +```text +Archive every rejected application older than 30 days, but show me the list and ask for confirmation before applying the bulk update. +``` + +## Attach sent documents + +Use document attachment when you want the tracker to store the exact resume PDF or cover-letter PDF you sent for an application. + +```text +Attach this PDF as the sent resume for the Acme application, then confirm the application now has a resume document. +``` + +```text +Attach this cover letter PDF to the Globex Staff Engineer application. +``` + +```text +Replace the resume PDF on the Stripe application with this updated PDF. Add a note that I resent the revised resume. +``` + +```text +Remove the cover-letter PDF from the Acme application, but keep the application record and timeline. +``` + + + `attach_application_document` accepts base64-encoded PDF bytes and `contentType: "application/pdf"`. Some MCP clients hide that detail when they can read local files. If your client cannot read local files, upload the PDF from the web app instead. + + +## Run Application Copilot + +Application Copilot tools let an agent use the same AI workflows available in the application detail panel. + +```text +Score the resume linked to my Acme application against the saved job description. Summarize the biggest match gaps and do not change my resume. +``` + +```text +Create a tailored resume copy for the Globex Staff Engineer application. Keep the original resume unchanged, link the tailored copy back to the application, and tell me the new resume name. +``` + +```text +Draft a cover letter for the Stripe application using the linked resume and job description. Keep it concise and specific to the role. +``` + +```text +Draft a follow-up email for the recruiter on the Acme application. Mention that I enjoyed the technical screen and ask about next steps. Do not mark the email as sent. +``` + +```text +Review all interview-stage applications with linked resumes. For each one, score the match and list the top three tailoring opportunities. +``` + + + Review AI-generated resumes, cover letters, and messages before sending them. MCP tools can draft and save context, but you are responsible for the final content. + + +## Maintain your pipeline + +Ask the agent to do periodic cleanup with explicit confirmation before broad changes. + +```text +Find applications that have not changed in 21 days. Group them by stage and recommend which ones need a follow-up, archive, or no action. +``` + +```text +Add the tag needs-follow-up to every active application with a follow-up date before today. Show me the list before updating. +``` + +```text +Move all applications tagged offer to offer stage, unless they are already archived. +``` + +```text +Archive rejected applications older than 60 days. Ask for confirmation before making changes. +``` + +```text +Delete these duplicate application records after confirming which one has the most complete timeline and documents. +``` + +## Prompt library + +Use these prompts as starting points. Replace company names, roles, tags, dates, and file references with your own details. + +### Daily review + +```text +Give me a daily application tracker brief. Include applications needing follow-up today, interviews coming up, stale saved roles, and any applications missing a linked resume. +``` + +```text +Show my active applications in table form with company, role, stage, source, tags, follow-up date, and last updated time. +``` + +```text +Which applications are missing job descriptions, contacts, sent resume PDFs, or cover-letter PDFs? +``` + +### Research and capture + +```text +Create a saved application from this job post. Extract company, role, location, salary, source URL, and job description. Add tags for the main technologies mentioned. +``` + +```text +I am considering this role but have not applied. Add it as saved, link my backend resume, and add a note with the three reasons it looks relevant. +``` + +```text +Add this recruiter contact to the matching application and note that they reached out on LinkedIn today. +``` + +### Applying + +```text +Move the Acme application from saved to applied. Set applied date to today, attach the resume PDF I sent, and add a note with the application portal confirmation number. +``` + +```text +I just applied to three roles. Create applications for each one, tag them applied-today, and remind me to follow up in one week. +``` + +```text +Find the best resume to link to this application based on role title and tags. Ask me before updating the application. +``` + +### Interviews and follow-ups + +```text +Move the Globex application to screening and add a recruiter screen contact with the recruiter's name and email. +``` + +```text +Add a note that the onsite interview is scheduled for July 18. Set the follow-up date to July 19. +``` + +```text +Draft a short follow-up after my interview. Use the application's company, role, recruiter contact, and timeline notes. +``` + +### Reporting + +```text +Summarize my job search this month: number applied, interviews, offers, rejections, top sources, and response rate. +``` + +```text +Which sources are producing interviews? Compare LinkedIn, referrals, company sites, recruiters, and other sources. +``` + +```text +Show applications by stage and tell me where the pipeline is blocked. +``` + +## Suggested agent instruction + +Add this to your MCP client's project or session instructions when you want the agent to manage applications safely: + +```text +Use Reactive Resume MCP for application tracking. Start by calling list_applications before creating a new record so you do not duplicate existing applications. Confirm before delete_application, bulk_delete_applications, bulk_update_applications, replacing attached documents, or archiving more than five applications. Prefer add_application_note for timeline updates. Use Application Copilot tools only when the application has enough context, and never present AI-generated cover letters or follow-ups as sent messages. +``` + +## Troubleshooting + +| Issue | What to do | +| --- | --- | +| The agent cannot see application tools | Reconnect the MCP server and ask the client to refresh tool discovery. Confirm you are connected to Reactive Resume v5.2.2 or later. | +| The agent creates duplicates | Ask it to run `list_applications` first and match by company, role, and source URL before creating records. | +| Document attachment fails | Attach only PDFs. If your MCP client cannot read local files, upload the document from the web app. | +| Match scoring or tailoring fails | Link a Reactive Resume to the application and add a job description. Confirm your AI provider is configured. | +| A bulk action changed too much | Use the application list and timeline to inspect what changed. For future sessions, require confirmation before bulk actions. | diff --git a/docs/guides/tracking-job-applications.mdx b/docs/guides/tracking-job-applications.mdx index 5615cda57..dd487615e 100644 --- a/docs/guides/tracking-job-applications.mdx +++ b/docs/guides/tracking-job-applications.mdx @@ -148,6 +148,12 @@ Match scoring and resume tailoring require both a linked Reactive Resume and a j Review AI-generated content before sending it. You are responsible for the final resume, cover letter, and follow-up message. +## Manage applications from an MCP client + +You can also manage the Application Tracker from an MCP-compatible AI client. This lets an agent list applications, create new records, move stages, add notes, attach the PDFs you sent, and run Application Copilot without opening the web app. + +For setup and prompt examples, see [Managing applications with MCP](/guides/managing-applications-with-mcp). + ## Archive or delete applications Archive applications you want to hide from the active board without deleting their history. Use the **Archived** toggle to view archived applications and unarchive them later. diff --git a/docs/guides/using-the-mcp-server.mdx b/docs/guides/using-the-mcp-server.mdx index 838165c04..249bb38fe 100644 --- a/docs/guides/using-the-mcp-server.mdx +++ b/docs/guides/using-the-mcp-server.mdx @@ -3,7 +3,7 @@ title: "Using the MCP Server" description: "Connect Reactive Resume to AI tools like Claude Desktop, Cursor, and Codex using the Model Context Protocol" --- -The Reactive Resume MCP server lets you manage and modify your resumes through any MCP-compatible AI tool — Claude Desktop, Cursor, Codex, and more. It connects to the Reactive Resume API and exposes tools for listing, reading, and patching resumes using natural language. +The Reactive Resume MCP server lets you manage and modify your resumes and job applications through any MCP-compatible AI tool - Claude Desktop, Cursor, Codex, and more. It connects to the Reactive Resume API and exposes tools for resume editing, Application Tracker workflows, and AI-assisted job application tasks using natural language. ## What is MCP? @@ -224,6 +224,23 @@ Tool names use canonical unprefixed `snake_case` names. | `lock_resume` | Lock a resume to prevent edits, patches, and deletion | | `unlock_resume` | Unlock a previously locked resume to re-enable editing | | `get_resume_statistics` | Get view and download statistics for a resume | +| `list_applications` | List tracked job applications. Supports stage, tag, and archived filters | +| `read_application` | Read one full application record with contacts, follow-up details, documents, and timeline | +| `list_application_tags` | List every distinct tag used across applications | +| `get_application_stats` | Get aggregate application counts by stage and source | +| `create_application` | Create a tracked job application | +| `update_application` | Update fields, move stage, archive/unarchive, edit contacts/follow-ups/tags, or link a resume | +| `add_application_note` | Append a note to an application's activity timeline | +| `delete_application` | Permanently delete one application and its owned uploaded documents | +| `bulk_update_applications` | Move, archive/unarchive, or add tags to multiple applications | +| `bulk_delete_applications` | Permanently delete multiple applications | +| `import_applications` | Bulk-create parsed application rows, up to 500 items | +| `attach_application_document` | Attach a sent resume or cover-letter PDF from base64-encoded PDF bytes | +| `remove_application_document` | Remove a sent resume or cover-letter PDF | +| `autofill_application_from_job` | Use AI to extract job details from a URL or pasted job description | +| `score_application_match` | Score the linked resume against the application job description | +| `tailor_resume_for_application` | Create and link a tailored resume copy for an application | +| `draft_application_message` | Draft a cover letter or recruiter follow-up from application and resume context | ### Breaking change (tool names) @@ -273,6 +290,18 @@ Once your MCP client is connected, you can use natural language to interact with - "What skills are listed on my resume?" - "Show me the stats for my resume" +### Tracking Applications + +- "Create an application for Senior Frontend Engineer at Acme, stage saved, source LinkedIn." +- "List my archived applications tagged remote." +- "Move my Acme application to interview and add a note that the technical screen is next Tuesday." +- "Attach this resume PDF to the Acme application." +- "Score the resume linked to this application against the job description." +- "Create a tailored resume copy for this application." +- "Draft a follow-up message for the recruiter." + +For a complete workflow and prompt library, see [Managing applications with MCP](/guides/managing-applications-with-mcp). + ### Creating & Managing - "Create a new resume called 'Frontend Engineer 2026'" @@ -320,3 +349,4 @@ Once your MCP client is connected, you can use natural language to interact with | "API error (403)" | The resume is locked. Unlock it in the Reactive Resume dashboard | | Connection refused | Check that the URL is correct and the instance is running | | "ReferenceError: File is not defined" when using `mcp-remote` | You're running Node.js 18. `mcp-remote` requires **Node.js 20 or later** — upgrade with `nvm use 20` or `nvm alias default 20` | +| "Application documents must be PDF files" | `attach_application_document` only accepts `contentType: "application/pdf"` and base64-encoded PDF bytes | diff --git a/docs/spec.json b/docs/spec.json index 266187910..79a227fdd 100644 --- a/docs/spec.json +++ b/docs/spec.json @@ -1,13 +1,27 @@ { "info": { "title": "Reactive Resume", - "version": "5.1.0", + "version": "5.2.2", "description": "Reactive Resume API", - "license": { "name": "MIT", "url": "https://github.com/amruthpillai/reactive-resume/blob/main/LICENSE" }, - "contact": { "name": "Amruth Pillai", "email": "hello@amruthpillai.com", "url": "https://amruthpillai.com" } + "license": { + "name": "MIT", + "url": "https://github.com/amruthpillai/reactive-resume/blob/main/LICENSE" + }, + "contact": { + "name": "Amruth Pillai", + "email": "hello@amruthpillai.com", + "url": "https://amruthpillai.com" + } + }, + "servers": [ + { + "url": "http://localhost:3000/api/openapi" + } + ], + "externalDocs": { + "url": "https://docs.rxresu.me", + "description": "Reactive Resume Documentation" }, - "servers": [{ "url": "http://localhost:3000/api/openapi" }], - "externalDocs": { "url": "https://docs.rxresu.me", "description": "Reactive Resume Documentation" }, "components": { "securitySchemes": { "apiKey": { @@ -24,7 +38,10 @@ "picture": { "type": "object", "properties": { - "hidden": { "type": "boolean", "description": "Whether to hide the picture from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the picture from the resume." + }, "url": { "type": "string", "description": "The URL to the picture to display on the resume. Prefer local app-served paths (for example /uploads/...) populated via upload." @@ -89,11 +106,26 @@ "basics": { "type": "object", "properties": { - "name": { "type": "string", "description": "The full name of the author of the resume." }, - "headline": { "type": "string", "description": "The headline of the author of the resume." }, - "email": { "type": "string", "description": "The email address of the author of the resume." }, - "phone": { "type": "string", "description": "The phone number of the author of the resume." }, - "location": { "type": "string", "description": "The location of the author of the resume." }, + "name": { + "type": "string", + "description": "The full name of the author of the resume." + }, + "headline": { + "type": "string", + "description": "The headline of the author of the resume." + }, + "email": { + "type": "string", + "description": "The email address of the author of the resume." + }, + "phone": { + "type": "string", + "description": "The phone number of the author of the resume." + }, + "location": { + "type": "string", + "description": "The location of the author of the resume." + }, "website": { "type": "object", "properties": { @@ -122,7 +154,10 @@ "type": "string", "description": "The icon to display for the custom field. Must be a valid icon name from @phosphor-icons/web icon set, or an empty string to hide. Default to '' (empty string) when unsure which icons are available." }, - "text": { "type": "string", "description": "The text to display for the custom field." }, + "text": { + "type": "string", + "description": "The text to display for the custom field." + }, "link": { "type": "string", "description": "If the custom field should be a link, the URL to link to." @@ -139,20 +174,38 @@ "summary": { "type": "object", "properties": { - "title": { "type": "string", "description": "The title of the summary of the resume." }, + "title": { + "type": "string", + "description": "The title of the summary of the resume." + }, + "icon": { + "type": "string", + "description": "Phosphor icon name to display before the summary section title in the PDF output. Empty string uses the default summary icon; 'none' hides the icon." + }, "columns": { "type": "integer", "minimum": 1, "maximum": 6, "description": "The number of columns the summary should span across." }, - "hidden": { "type": "boolean", "description": "Whether to hide the summary from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the summary from the resume." + }, + "keepTogether": { + "type": "boolean", + "description": "If true, the summary is kept on a single page instead of splitting across a page break." + }, + "startOnNewPage": { + "type": "boolean", + "description": "If true, the summary always begins on a new page." + }, "content": { "type": "string", "description": "The content of the summary of the resume. This should be a HTML-formatted string." } }, - "required": ["title", "columns", "hidden", "content"], + "required": ["title", "icon", "columns", "hidden", "keepTogether", "startOnNewPage", "content"], "description": "Summary section of the resume, useful for a short bio or introduction" }, "sections": { @@ -161,14 +214,32 @@ "profiles": { "type": "object", "properties": { - "title": { "type": "string", "description": "The title of the section." }, + "title": { + "type": "string", + "description": "The title of the section." + }, + "icon": { + "type": "string", + "description": "Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon." + }, "columns": { "type": "integer", "minimum": 1, "maximum": 6, "description": "The number of columns the section should span across." }, - "hidden": { "type": "boolean", "description": "Whether to hide the section from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the section from the resume." + }, + "keepTogether": { + "type": "boolean", + "description": "If true, the section is kept on a single page instead of splitting across a page break." + }, + "startOnNewPage": { + "type": "boolean", + "description": "If true, the section always begins on a new page." + }, "items": { "type": "array", "items": { @@ -178,7 +249,10 @@ "type": "string", "description": "The unique identifier for the item. Usually generated as a UUID." }, - "hidden": { "type": "boolean", "description": "Whether to hide the item from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the item from the resume." + }, "icon": { "type": "string", "description": "The icon to display for the custom field. Must be a valid icon name from @phosphor-icons/web icon set, or an empty string to hide. Default to '' (empty string) when unsure which icons are available." @@ -221,20 +295,38 @@ "description": "The items to display in the profiles section." } }, - "required": ["title", "columns", "hidden", "items"], + "required": ["title", "icon", "columns", "hidden", "keepTogether", "startOnNewPage", "items"], "description": "The section to display the profiles of the author." }, "experience": { "type": "object", "properties": { - "title": { "type": "string", "description": "The title of the section." }, + "title": { + "type": "string", + "description": "The title of the section." + }, + "icon": { + "type": "string", + "description": "Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon." + }, "columns": { "type": "integer", "minimum": 1, "maximum": 6, "description": "The number of columns the section should span across." }, - "hidden": { "type": "boolean", "description": "Whether to hide the section from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the section from the resume." + }, + "keepTogether": { + "type": "boolean", + "description": "If true, the section is kept on a single page instead of splitting across a page break." + }, + "startOnNewPage": { + "type": "boolean", + "description": "If true, the section always begins on a new page." + }, "items": { "type": "array", "items": { @@ -244,7 +336,10 @@ "type": "string", "description": "The unique identifier for the item. Usually generated as a UUID." }, - "hidden": { "type": "boolean", "description": "Whether to hide the item from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the item from the resume." + }, "company": { "type": "string", "minLength": 1, @@ -254,7 +349,10 @@ "type": "string", "description": "The position held at the company or organization. Used when there is only a single role. If multiple roles are provided in the 'roles' field, this serves as a summary title or can be left blank." }, - "location": { "type": "string", "description": "The location of the company or organization." }, + "location": { + "type": "string", + "description": "The location of the company or organization." + }, "period": { "type": "string", "description": "The overall period of time at the company. When multiple roles are used, this should reflect the total tenure." @@ -295,7 +393,10 @@ "type": "string", "description": "The position or job title for this role." }, - "period": { "type": "string", "description": "The period of time this role was held." }, + "period": { + "type": "string", + "description": "The period of time this role was held." + }, "description": { "type": "string", "description": "The description of this specific role. This should be a HTML-formatted string." @@ -321,20 +422,38 @@ "description": "The items to display in the experience section." } }, - "required": ["title", "columns", "hidden", "items"], + "required": ["title", "icon", "columns", "hidden", "keepTogether", "startOnNewPage", "items"], "description": "The section to display the experience of the author." }, "education": { "type": "object", "properties": { - "title": { "type": "string", "description": "The title of the section." }, + "title": { + "type": "string", + "description": "The title of the section." + }, + "icon": { + "type": "string", + "description": "Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon." + }, "columns": { "type": "integer", "minimum": 1, "maximum": 6, "description": "The number of columns the section should span across." }, - "hidden": { "type": "boolean", "description": "Whether to hide the section from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the section from the resume." + }, + "keepTogether": { + "type": "boolean", + "description": "If true, the section is kept on a single page instead of splitting across a page break." + }, + "startOnNewPage": { + "type": "boolean", + "description": "If true, the section always begins on a new page." + }, "items": { "type": "array", "items": { @@ -344,16 +463,31 @@ "type": "string", "description": "The unique identifier for the item. Usually generated as a UUID." }, - "hidden": { "type": "boolean", "description": "Whether to hide the item from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the item from the resume." + }, "school": { "type": "string", "minLength": 1, "description": "The name of the school or institution." }, - "degree": { "type": "string", "description": "The degree or qualification obtained." }, - "area": { "type": "string", "description": "The area of study or specialization." }, - "grade": { "type": "string", "description": "The grade or score achieved." }, - "location": { "type": "string", "description": "The location of the school or institution." }, + "degree": { + "type": "string", + "description": "The degree or qualification obtained." + }, + "area": { + "type": "string", + "description": "The area of study or specialization." + }, + "grade": { + "type": "string", + "description": "The grade or score achieved." + }, + "location": { + "type": "string", + "description": "The location of the school or institution." + }, "period": { "type": "string", "description": "The period of time the education was obtained over." @@ -398,20 +532,38 @@ "description": "The items to display in the education section." } }, - "required": ["title", "columns", "hidden", "items"], + "required": ["title", "icon", "columns", "hidden", "keepTogether", "startOnNewPage", "items"], "description": "The section to display the education of the author." }, "projects": { "type": "object", "properties": { - "title": { "type": "string", "description": "The title of the section." }, + "title": { + "type": "string", + "description": "The title of the section." + }, + "icon": { + "type": "string", + "description": "Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon." + }, "columns": { "type": "integer", "minimum": 1, "maximum": 6, "description": "The number of columns the section should span across." }, - "hidden": { "type": "boolean", "description": "Whether to hide the section from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the section from the resume." + }, + "keepTogether": { + "type": "boolean", + "description": "If true, the section is kept on a single page instead of splitting across a page break." + }, + "startOnNewPage": { + "type": "boolean", + "description": "If true, the section always begins on a new page." + }, "items": { "type": "array", "items": { @@ -421,9 +573,19 @@ "type": "string", "description": "The unique identifier for the item. Usually generated as a UUID." }, - "hidden": { "type": "boolean", "description": "Whether to hide the item from the resume." }, - "name": { "type": "string", "minLength": 1, "description": "The name of the project." }, - "period": { "type": "string", "description": "The period of time the project was worked on." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the item from the resume." + }, + "name": { + "type": "string", + "minLength": 1, + "description": "The name of the project." + }, + "period": { + "type": "string", + "description": "The period of time the project was worked on." + }, "website": { "type": "object", "properties": { @@ -453,20 +615,38 @@ "description": "The items to display in the projects section." } }, - "required": ["title", "columns", "hidden", "items"], + "required": ["title", "icon", "columns", "hidden", "keepTogether", "startOnNewPage", "items"], "description": "The section to display the projects of the author." }, "skills": { "type": "object", "properties": { - "title": { "type": "string", "description": "The title of the section." }, + "title": { + "type": "string", + "description": "The title of the section." + }, + "icon": { + "type": "string", + "description": "Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon." + }, "columns": { "type": "integer", "minimum": 1, "maximum": 6, "description": "The number of columns the section should span across." }, - "hidden": { "type": "boolean", "description": "Whether to hide the section from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the section from the resume." + }, + "keepTogether": { + "type": "boolean", + "description": "If true, the section is kept on a single page instead of splitting across a page break." + }, + "startOnNewPage": { + "type": "boolean", + "description": "If true, the section always begins on a new page." + }, "items": { "type": "array", "items": { @@ -476,7 +656,10 @@ "type": "string", "description": "The unique identifier for the item. Usually generated as a UUID." }, - "hidden": { "type": "boolean", "description": "Whether to hide the item from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the item from the resume." + }, "icon": { "type": "string", "description": "The icon to display for the custom field. Must be a valid icon name from @phosphor-icons/web icon set, or an empty string to hide. Default to '' (empty string) when unsure which icons are available." @@ -485,7 +668,11 @@ "type": "string", "description": "Custom color for the icon, defined as rgba(r, g, b, a). Leave blank to use the template default icon color." }, - "name": { "type": "string", "minLength": 1, "description": "The name of the skill." }, + "name": { + "type": "string", + "minLength": 1, + "description": "The name of the skill." + }, "proficiency": { "type": "string", "description": "The proficiency level of the skill. Can be any text, such as 'Beginner', 'Intermediate', 'Advanced', etc." @@ -498,7 +685,9 @@ }, "keywords": { "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "description": "The keywords associated with the skill, if any. These are displayed as tags below the name." } }, @@ -507,20 +696,38 @@ "description": "The items to display in the skills section." } }, - "required": ["title", "columns", "hidden", "items"], + "required": ["title", "icon", "columns", "hidden", "keepTogether", "startOnNewPage", "items"], "description": "The section to display the skills of the author." }, "languages": { "type": "object", "properties": { - "title": { "type": "string", "description": "The title of the section." }, + "title": { + "type": "string", + "description": "The title of the section." + }, + "icon": { + "type": "string", + "description": "Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon." + }, "columns": { "type": "integer", "minimum": 1, "maximum": 6, "description": "The number of columns the section should span across." }, - "hidden": { "type": "boolean", "description": "Whether to hide the section from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the section from the resume." + }, + "keepTogether": { + "type": "boolean", + "description": "If true, the section is kept on a single page instead of splitting across a page break." + }, + "startOnNewPage": { + "type": "boolean", + "description": "If true, the section always begins on a new page." + }, "items": { "type": "array", "items": { @@ -530,7 +737,10 @@ "type": "string", "description": "The unique identifier for the item. Usually generated as a UUID." }, - "hidden": { "type": "boolean", "description": "Whether to hide the item from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the item from the resume." + }, "language": { "type": "string", "minLength": 1, @@ -552,20 +762,38 @@ "description": "The items to display in the languages section." } }, - "required": ["title", "columns", "hidden", "items"], + "required": ["title", "icon", "columns", "hidden", "keepTogether", "startOnNewPage", "items"], "description": "The section to display the languages of the author." }, "interests": { "type": "object", "properties": { - "title": { "type": "string", "description": "The title of the section." }, + "title": { + "type": "string", + "description": "The title of the section." + }, + "icon": { + "type": "string", + "description": "Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon." + }, "columns": { "type": "integer", "minimum": 1, "maximum": 6, "description": "The number of columns the section should span across." }, - "hidden": { "type": "boolean", "description": "Whether to hide the section from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the section from the resume." + }, + "keepTogether": { + "type": "boolean", + "description": "If true, the section is kept on a single page instead of splitting across a page break." + }, + "startOnNewPage": { + "type": "boolean", + "description": "If true, the section always begins on a new page." + }, "items": { "type": "array", "items": { @@ -575,7 +803,10 @@ "type": "string", "description": "The unique identifier for the item. Usually generated as a UUID." }, - "hidden": { "type": "boolean", "description": "Whether to hide the item from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the item from the resume." + }, "icon": { "type": "string", "description": "The icon to display for the custom field. Must be a valid icon name from @phosphor-icons/web icon set, or an empty string to hide. Default to '' (empty string) when unsure which icons are available." @@ -584,10 +815,16 @@ "type": "string", "description": "Custom color for the icon, defined as rgba(r, g, b, a). Leave blank to use the template default icon color." }, - "name": { "type": "string", "minLength": 1, "description": "The name of the interest/hobby." }, + "name": { + "type": "string", + "minLength": 1, + "description": "The name of the interest/hobby." + }, "keywords": { "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "description": "The keywords associated with the interest/hobby, if any. These are displayed as tags below the name." } }, @@ -596,20 +833,38 @@ "description": "The items to display in the interests section." } }, - "required": ["title", "columns", "hidden", "items"], + "required": ["title", "icon", "columns", "hidden", "keepTogether", "startOnNewPage", "items"], "description": "The section to display the interests of the author." }, "awards": { "type": "object", "properties": { - "title": { "type": "string", "description": "The title of the section." }, + "title": { + "type": "string", + "description": "The title of the section." + }, + "icon": { + "type": "string", + "description": "Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon." + }, "columns": { "type": "integer", "minimum": 1, "maximum": 6, "description": "The number of columns the section should span across." }, - "hidden": { "type": "boolean", "description": "Whether to hide the section from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the section from the resume." + }, + "keepTogether": { + "type": "boolean", + "description": "If true, the section is kept on a single page instead of splitting across a page break." + }, + "startOnNewPage": { + "type": "boolean", + "description": "If true, the section always begins on a new page." + }, "items": { "type": "array", "items": { @@ -619,10 +874,23 @@ "type": "string", "description": "The unique identifier for the item. Usually generated as a UUID." }, - "hidden": { "type": "boolean", "description": "Whether to hide the item from the resume." }, - "title": { "type": "string", "minLength": 1, "description": "The title of the award." }, - "awarder": { "type": "string", "description": "The awarder of the award." }, - "date": { "type": "string", "description": "The date when the award was received." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the item from the resume." + }, + "title": { + "type": "string", + "minLength": 1, + "description": "The title of the award." + }, + "awarder": { + "type": "string", + "description": "The awarder of the award." + }, + "date": { + "type": "string", + "description": "The date when the award was received." + }, "website": { "type": "object", "properties": { @@ -652,20 +920,38 @@ "description": "The items to display in the awards section." } }, - "required": ["title", "columns", "hidden", "items"], + "required": ["title", "icon", "columns", "hidden", "keepTogether", "startOnNewPage", "items"], "description": "The section to display the awards of the author." }, "certifications": { "type": "object", "properties": { - "title": { "type": "string", "description": "The title of the section." }, + "title": { + "type": "string", + "description": "The title of the section." + }, + "icon": { + "type": "string", + "description": "Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon." + }, "columns": { "type": "integer", "minimum": 1, "maximum": 6, "description": "The number of columns the section should span across." }, - "hidden": { "type": "boolean", "description": "Whether to hide the section from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the section from the resume." + }, + "keepTogether": { + "type": "boolean", + "description": "If true, the section is kept on a single page instead of splitting across a page break." + }, + "startOnNewPage": { + "type": "boolean", + "description": "If true, the section always begins on a new page." + }, "items": { "type": "array", "items": { @@ -675,10 +961,23 @@ "type": "string", "description": "The unique identifier for the item. Usually generated as a UUID." }, - "hidden": { "type": "boolean", "description": "Whether to hide the item from the resume." }, - "title": { "type": "string", "minLength": 1, "description": "The title of the certification." }, - "issuer": { "type": "string", "description": "The issuer of the certification." }, - "date": { "type": "string", "description": "The date when the certification was received." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the item from the resume." + }, + "title": { + "type": "string", + "minLength": 1, + "description": "The title of the certification." + }, + "issuer": { + "type": "string", + "description": "The issuer of the certification." + }, + "date": { + "type": "string", + "description": "The date when the certification was received." + }, "website": { "type": "object", "properties": { @@ -708,20 +1007,38 @@ "description": "The items to display in the certifications section." } }, - "required": ["title", "columns", "hidden", "items"], + "required": ["title", "icon", "columns", "hidden", "keepTogether", "startOnNewPage", "items"], "description": "The section to display the certifications of the author." }, "publications": { "type": "object", "properties": { - "title": { "type": "string", "description": "The title of the section." }, + "title": { + "type": "string", + "description": "The title of the section." + }, + "icon": { + "type": "string", + "description": "Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon." + }, "columns": { "type": "integer", "minimum": 1, "maximum": 6, "description": "The number of columns the section should span across." }, - "hidden": { "type": "boolean", "description": "Whether to hide the section from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the section from the resume." + }, + "keepTogether": { + "type": "boolean", + "description": "If true, the section is kept on a single page instead of splitting across a page break." + }, + "startOnNewPage": { + "type": "boolean", + "description": "If true, the section always begins on a new page." + }, "items": { "type": "array", "items": { @@ -731,10 +1048,23 @@ "type": "string", "description": "The unique identifier for the item. Usually generated as a UUID." }, - "hidden": { "type": "boolean", "description": "Whether to hide the item from the resume." }, - "title": { "type": "string", "minLength": 1, "description": "The title of the publication." }, - "publisher": { "type": "string", "description": "The publisher of the publication." }, - "date": { "type": "string", "description": "The date when the publication was published." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the item from the resume." + }, + "title": { + "type": "string", + "minLength": 1, + "description": "The title of the publication." + }, + "publisher": { + "type": "string", + "description": "The publisher of the publication." + }, + "date": { + "type": "string", + "description": "The date when the publication was published." + }, "website": { "type": "object", "properties": { @@ -764,20 +1094,38 @@ "description": "The items to display in the publications section." } }, - "required": ["title", "columns", "hidden", "items"], + "required": ["title", "icon", "columns", "hidden", "keepTogether", "startOnNewPage", "items"], "description": "The section to display the publications of the author." }, "volunteer": { "type": "object", "properties": { - "title": { "type": "string", "description": "The title of the section." }, + "title": { + "type": "string", + "description": "The title of the section." + }, + "icon": { + "type": "string", + "description": "Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon." + }, "columns": { "type": "integer", "minimum": 1, "maximum": 6, "description": "The number of columns the section should span across." }, - "hidden": { "type": "boolean", "description": "Whether to hide the section from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the section from the resume." + }, + "keepTogether": { + "type": "boolean", + "description": "If true, the section is kept on a single page instead of splitting across a page break." + }, + "startOnNewPage": { + "type": "boolean", + "description": "If true, the section always begins on a new page." + }, "items": { "type": "array", "items": { @@ -787,13 +1135,19 @@ "type": "string", "description": "The unique identifier for the item. Usually generated as a UUID." }, - "hidden": { "type": "boolean", "description": "Whether to hide the item from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the item from the resume." + }, "organization": { "type": "string", "minLength": 1, "description": "The name of the organization or company." }, - "location": { "type": "string", "description": "The location of the organization or company." }, + "location": { + "type": "string", + "description": "The location of the organization or company." + }, "period": { "type": "string", "description": "The period of time the author was volunteered at the organization or company." @@ -827,20 +1181,38 @@ "description": "The items to display in the volunteer section." } }, - "required": ["title", "columns", "hidden", "items"], + "required": ["title", "icon", "columns", "hidden", "keepTogether", "startOnNewPage", "items"], "description": "The section to display the volunteer experience of the author." }, "references": { "type": "object", "properties": { - "title": { "type": "string", "description": "The title of the section." }, + "title": { + "type": "string", + "description": "The title of the section." + }, + "icon": { + "type": "string", + "description": "Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon." + }, "columns": { "type": "integer", "minimum": 1, "maximum": 6, "description": "The number of columns the section should span across." }, - "hidden": { "type": "boolean", "description": "Whether to hide the section from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the section from the resume." + }, + "keepTogether": { + "type": "boolean", + "description": "If true, the section is kept on a single page instead of splitting across a page break." + }, + "startOnNewPage": { + "type": "boolean", + "description": "If true, the section always begins on a new page." + }, "items": { "type": "array", "items": { @@ -850,13 +1222,19 @@ "type": "string", "description": "The unique identifier for the item. Usually generated as a UUID." }, - "hidden": { "type": "boolean", "description": "Whether to hide the item from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the item from the resume." + }, "name": { "type": "string", "minLength": 1, "description": "The name of the reference, or a note such as 'Available upon request'." }, - "position": { "type": "string", "description": "The position or job title of the reference." }, + "position": { + "type": "string", + "description": "The position or job title of the reference." + }, "website": { "type": "object", "properties": { @@ -876,7 +1254,10 @@ "required": ["url", "label", "inlineLink"], "description": "The website or LinkedIn profile of the reference, if any." }, - "phone": { "type": "string", "description": "The phone number of the reference." }, + "phone": { + "type": "string", + "description": "The phone number of the reference." + }, "description": { "type": "string", "description": "The description of the reference. Can be used to display a quote, a testimonial, etc. This should be a HTML-formatted string." @@ -887,7 +1268,7 @@ "description": "The items to display in the references section." } }, - "required": ["title", "columns", "hidden", "items"], + "required": ["title", "icon", "columns", "hidden", "keepTogether", "startOnNewPage", "items"], "description": "The section to display the references of the author." } }, @@ -912,14 +1293,32 @@ "items": { "type": "object", "properties": { - "title": { "type": "string", "description": "The title of the section." }, + "title": { + "type": "string", + "description": "The title of the section." + }, + "icon": { + "type": "string", + "description": "Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon." + }, "columns": { "type": "integer", "minimum": 1, "maximum": 6, "description": "The number of columns the section should span across." }, - "hidden": { "type": "boolean", "description": "Whether to hide the section from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the section from the resume." + }, + "keepTogether": { + "type": "boolean", + "description": "If true, the section is kept on a single page instead of splitting across a page break." + }, + "startOnNewPage": { + "type": "boolean", + "description": "If true, the section always begins on a new page." + }, "id": { "type": "string", "description": "The unique identifier for the custom section. Usually generated as a UUID." @@ -955,7 +1354,10 @@ "type": "string", "description": "The unique identifier for the item. Usually generated as a UUID." }, - "hidden": { "type": "boolean", "description": "Whether to hide the item from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the item from the resume." + }, "recipient": { "type": "string", "description": "The recipient's address block as HTML (name, title, company, address, email)." @@ -974,7 +1376,10 @@ "type": "string", "description": "The unique identifier for the item. Usually generated as a UUID." }, - "hidden": { "type": "boolean", "description": "Whether to hide the item from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the item from the resume." + }, "content": { "type": "string", "description": "The rich text content of the summary item. This should be a HTML-formatted string." @@ -989,7 +1394,10 @@ "type": "string", "description": "The unique identifier for the item. Usually generated as a UUID." }, - "hidden": { "type": "boolean", "description": "Whether to hide the item from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the item from the resume." + }, "icon": { "type": "string", "description": "The icon to display for the custom field. Must be a valid icon name from @phosphor-icons/web icon set, or an empty string to hide. Default to '' (empty string) when unsure which icons are available." @@ -1036,7 +1444,10 @@ "type": "string", "description": "The unique identifier for the item. Usually generated as a UUID." }, - "hidden": { "type": "boolean", "description": "Whether to hide the item from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the item from the resume." + }, "company": { "type": "string", "minLength": 1, @@ -1090,7 +1501,10 @@ "type": "string", "description": "The position or job title for this role." }, - "period": { "type": "string", "description": "The period of time this role was held." }, + "period": { + "type": "string", + "description": "The period of time this role was held." + }, "description": { "type": "string", "description": "The description of this specific role. This should be a HTML-formatted string." @@ -1120,16 +1534,31 @@ "type": "string", "description": "The unique identifier for the item. Usually generated as a UUID." }, - "hidden": { "type": "boolean", "description": "Whether to hide the item from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the item from the resume." + }, "school": { "type": "string", "minLength": 1, "description": "The name of the school or institution." }, - "degree": { "type": "string", "description": "The degree or qualification obtained." }, - "area": { "type": "string", "description": "The area of study or specialization." }, - "grade": { "type": "string", "description": "The grade or score achieved." }, - "location": { "type": "string", "description": "The location of the school or institution." }, + "degree": { + "type": "string", + "description": "The degree or qualification obtained." + }, + "area": { + "type": "string", + "description": "The area of study or specialization." + }, + "grade": { + "type": "string", + "description": "The grade or score achieved." + }, + "location": { + "type": "string", + "description": "The location of the school or institution." + }, "period": { "type": "string", "description": "The period of time the education was obtained over." @@ -1178,8 +1607,15 @@ "type": "string", "description": "The unique identifier for the item. Usually generated as a UUID." }, - "hidden": { "type": "boolean", "description": "Whether to hide the item from the resume." }, - "name": { "type": "string", "minLength": 1, "description": "The name of the project." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the item from the resume." + }, + "name": { + "type": "string", + "minLength": 1, + "description": "The name of the project." + }, "period": { "type": "string", "description": "The period of time the project was worked on." @@ -1217,7 +1653,10 @@ "type": "string", "description": "The unique identifier for the item. Usually generated as a UUID." }, - "hidden": { "type": "boolean", "description": "Whether to hide the item from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the item from the resume." + }, "icon": { "type": "string", "description": "The icon to display for the custom field. Must be a valid icon name from @phosphor-icons/web icon set, or an empty string to hide. Default to '' (empty string) when unsure which icons are available." @@ -1226,7 +1665,11 @@ "type": "string", "description": "Custom color for the icon, defined as rgba(r, g, b, a). Leave blank to use the template default icon color." }, - "name": { "type": "string", "minLength": 1, "description": "The name of the skill." }, + "name": { + "type": "string", + "minLength": 1, + "description": "The name of the skill." + }, "proficiency": { "type": "string", "description": "The proficiency level of the skill. Can be any text, such as 'Beginner', 'Intermediate', 'Advanced', etc." @@ -1239,7 +1682,9 @@ }, "keywords": { "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "description": "The keywords associated with the skill, if any. These are displayed as tags below the name." } }, @@ -1252,7 +1697,10 @@ "type": "string", "description": "The unique identifier for the item. Usually generated as a UUID." }, - "hidden": { "type": "boolean", "description": "Whether to hide the item from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the item from the resume." + }, "language": { "type": "string", "minLength": 1, @@ -1278,7 +1726,10 @@ "type": "string", "description": "The unique identifier for the item. Usually generated as a UUID." }, - "hidden": { "type": "boolean", "description": "Whether to hide the item from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the item from the resume." + }, "icon": { "type": "string", "description": "The icon to display for the custom field. Must be a valid icon name from @phosphor-icons/web icon set, or an empty string to hide. Default to '' (empty string) when unsure which icons are available." @@ -1294,7 +1745,9 @@ }, "keywords": { "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "description": "The keywords associated with the interest/hobby, if any. These are displayed as tags below the name." } }, @@ -1307,10 +1760,23 @@ "type": "string", "description": "The unique identifier for the item. Usually generated as a UUID." }, - "hidden": { "type": "boolean", "description": "Whether to hide the item from the resume." }, - "title": { "type": "string", "minLength": 1, "description": "The title of the award." }, - "awarder": { "type": "string", "description": "The awarder of the award." }, - "date": { "type": "string", "description": "The date when the award was received." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the item from the resume." + }, + "title": { + "type": "string", + "minLength": 1, + "description": "The title of the award." + }, + "awarder": { + "type": "string", + "description": "The awarder of the award." + }, + "date": { + "type": "string", + "description": "The date when the award was received." + }, "website": { "type": "object", "properties": { @@ -1344,14 +1810,23 @@ "type": "string", "description": "The unique identifier for the item. Usually generated as a UUID." }, - "hidden": { "type": "boolean", "description": "Whether to hide the item from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the item from the resume." + }, "title": { "type": "string", "minLength": 1, "description": "The title of the certification." }, - "issuer": { "type": "string", "description": "The issuer of the certification." }, - "date": { "type": "string", "description": "The date when the certification was received." }, + "issuer": { + "type": "string", + "description": "The issuer of the certification." + }, + "date": { + "type": "string", + "description": "The date when the certification was received." + }, "website": { "type": "object", "properties": { @@ -1385,10 +1860,23 @@ "type": "string", "description": "The unique identifier for the item. Usually generated as a UUID." }, - "hidden": { "type": "boolean", "description": "Whether to hide the item from the resume." }, - "title": { "type": "string", "minLength": 1, "description": "The title of the publication." }, - "publisher": { "type": "string", "description": "The publisher of the publication." }, - "date": { "type": "string", "description": "The date when the publication was published." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the item from the resume." + }, + "title": { + "type": "string", + "minLength": 1, + "description": "The title of the publication." + }, + "publisher": { + "type": "string", + "description": "The publisher of the publication." + }, + "date": { + "type": "string", + "description": "The date when the publication was published." + }, "website": { "type": "object", "properties": { @@ -1422,7 +1910,10 @@ "type": "string", "description": "The unique identifier for the item. Usually generated as a UUID." }, - "hidden": { "type": "boolean", "description": "Whether to hide the item from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the item from the resume." + }, "organization": { "type": "string", "minLength": 1, @@ -1469,7 +1960,10 @@ "type": "string", "description": "The unique identifier for the item. Usually generated as a UUID." }, - "hidden": { "type": "boolean", "description": "Whether to hide the item from the resume." }, + "hidden": { + "type": "boolean", + "description": "Whether to hide the item from the resume." + }, "name": { "type": "string", "minLength": 1, @@ -1498,7 +1992,10 @@ "required": ["url", "label", "inlineLink"], "description": "The website or LinkedIn profile of the reference, if any." }, - "phone": { "type": "string", "description": "The phone number of the reference." }, + "phone": { + "type": "string", + "description": "The phone number of the reference." + }, "description": { "type": "string", "description": "The description of the reference. Can be used to display a quote, a testimonial, etc. This should be a HTML-formatted string." @@ -1511,7 +2008,17 @@ "description": "The items to display in the custom section. Items follow the schema of the section type." } }, - "required": ["title", "columns", "hidden", "id", "type", "items"] + "required": [ + "title", + "icon", + "columns", + "hidden", + "keepTogether", + "startOnNewPage", + "id", + "type", + "items" + ] }, "description": "Custom sections of the resume, such as a custom section for notes, etc." }, @@ -1559,12 +2066,16 @@ }, "main": { "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "description": "The items to display in the main column of the page. A string array of section IDs (experience, education, projects, skills, languages, interests, awards, certifications, publications, volunteer, references, profiles, summary or UUIDs for custom sections)." }, "sidebar": { "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "description": "The items to display in the sidebar column of the page. A string array of section IDs (experience, education, projects, skills, languages, interests, awards, certifications, publications, volunteer, references, profiles, summary or UUIDs for custom sections)." } }, @@ -1600,17 +2111,38 @@ "description": "The vertical margin of the page, defined in points (pt)." }, "format": { - "enum": ["a4", "letter"], + "enum": ["a4", "letter", "free-form"], "type": "string", - "description": "The format of the page. Can be 'a4' or 'letter'." + "description": "The format of the page. Can be 'a4', 'letter', or 'free-form'." }, "locale": { "type": "string", "description": "The locale of the page. Used for displaying pre-translated section headings, if not overridden." }, - "hideIcons": { "type": "boolean", "description": "Whether to hide the icons of the sections." } + "hideLinkUnderline": { + "type": "boolean", + "description": "Whether to hide the underlines of the links." + }, + "hideIcons": { + "type": "boolean", + "description": "Whether to hide the item-level icons (skills, profiles, interests)." + }, + "hideSectionIcons": { + "type": "boolean", + "description": "Whether to hide the section heading icons displayed before section titles." + } }, - "required": ["gapX", "gapY", "marginX", "marginY", "format", "locale", "hideIcons"], + "required": [ + "gapX", + "gapY", + "marginX", + "marginY", + "format", + "locale", + "hideLinkUnderline", + "hideIcons", + "hideSectionIcons" + ], "description": "The page settings of the resume. Determines the margins, format, and locale of the resume." }, "design": { @@ -1725,9 +2257,245 @@ "notes": { "type": "string", "description": "Personal notes for the resume. Can be used to add any additional information or instructions for the resume. These notes are not displayed on the resume, they are only visible to the author of the resume when editing the resume. This should be a HTML-formatted string." + }, + "styleRules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1, + "description": "Unique identifier for this style rule." + }, + "label": { + "type": "string", + "description": "Human-readable label for this style rule." + }, + "enabled": { + "type": "boolean", + "description": "Whether this style rule should affect PDF rendering." + }, + "target": { + "anyOf": [ + { + "type": "object", + "properties": { + "scope": { + "const": "global" + } + }, + "required": ["scope"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "scope": { + "const": "sectionType" + }, + "sectionType": { + "enum": [ + "summary", + "profiles", + "experience", + "education", + "projects", + "skills", + "languages", + "interests", + "awards", + "certifications", + "publications", + "volunteer", + "references", + "cover-letter" + ], + "type": "string" + } + }, + "required": ["scope", "sectionType"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "scope": { + "const": "sectionId" + }, + "sectionId": { + "type": "string", + "minLength": 1 + } + }, + "required": ["scope", "sectionId"], + "additionalProperties": false + } + ], + "description": "The resume content this style rule applies to." + }, + "slots": { + "type": "object", + "propertyNames": { + "enum": [ + "section", + "heading", + "item", + "text", + "secondaryText", + "link", + "icon", + "level", + "richParagraph", + "richList", + "richListItemRow", + "richListItemContent", + "richLink", + "richBold", + "richMark" + ], + "type": "string" + }, + "additionalProperties": { + "type": "object", + "properties": { + "color": { + "type": "string" + }, + "backgroundColor": { + "type": "string" + }, + "borderColor": { + "type": "string" + }, + "textDecorationColor": { + "type": "string" + }, + "opacity": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "fontSize": { + "type": "number", + "minimum": 6, + "maximum": 48 + }, + "fontWeight": { + "enum": ["100", "200", "300", "400", "500", "600", "700", "800", "900"], + "type": "string" + }, + "fontStyle": { + "enum": ["normal", "italic"], + "type": "string" + }, + "lineHeight": { + "type": "number", + "minimum": 0.5, + "maximum": 4 + }, + "letterSpacing": { + "type": "number", + "minimum": -16, + "maximum": 16 + }, + "textDecoration": { + "enum": ["none", "underline", "line-through"], + "type": "string" + }, + "textDecorationStyle": { + "enum": ["solid", "dashed", "dotted"], + "type": "string" + }, + "textAlign": { + "enum": ["left", "center", "right", "justify"], + "type": "string" + }, + "textTransform": { + "enum": ["none", "uppercase", "lowercase", "capitalize"], + "type": "string" + }, + "padding": { + "type": "number", + "minimum": -72, + "maximum": 72 + }, + "paddingTop": { + "type": "number", + "minimum": -72, + "maximum": 72 + }, + "paddingRight": { + "type": "number", + "minimum": -72, + "maximum": 72 + }, + "paddingBottom": { + "type": "number", + "minimum": -72, + "maximum": 72 + }, + "paddingLeft": { + "type": "number", + "minimum": -72, + "maximum": 72 + }, + "marginTop": { + "type": "number", + "minimum": -72, + "maximum": 72 + }, + "marginRight": { + "type": "number", + "minimum": -72, + "maximum": 72 + }, + "marginBottom": { + "type": "number", + "minimum": -72, + "maximum": 72 + }, + "marginLeft": { + "type": "number", + "minimum": -72, + "maximum": 72 + }, + "rowGap": { + "type": "number", + "minimum": -72, + "maximum": 72 + }, + "columnGap": { + "type": "number", + "minimum": -72, + "maximum": 72 + }, + "borderStyle": { + "enum": ["solid", "dashed", "dotted"], + "type": "string" + }, + "borderWidth": { + "type": "number", + "minimum": 0 + }, + "borderRadius": { + "type": "number", + "minimum": 0 + } + }, + "additionalProperties": false, + "description": "Constrained visual style intent that can be safely translated to React PDF styles." + }, + "description": "The semantic style slots configured by this rule." + } + }, + "required": ["id", "label", "enabled", "target", "slots"], + "additionalProperties": false + }, + "description": "Structured style rules that target semantic resume sections and slots for React PDF rendering." } }, - "required": ["template", "layout", "page", "design", "typography", "notes"], + "required": ["template", "layout", "page", "design", "typography", "notes", "styleRules"], "description": "Metadata for the resume, such as template, layout, typography, etc. This section describes the overall design and appearance of the resume." } }, @@ -1736,112 +2504,13 @@ } } }, - "security": [{ "apiKey": [] }], + "security": [ + { + "apiKey": [] + } + ], "openapi": "3.1.1", "paths": { - "/ai/test-connection": { - "post": { - "operationId": "testAiConnection", - "summary": "Test AI provider connection", - "description": "Validates the connection to an AI provider by sending a simple test prompt. Requires the provider type, model name, API key, and an optional base URL. Supported providers: OpenAI, Anthropic, Google Gemini, Ollama, OpenRouter, and Vercel AI Gateway. Requires authentication.", - "tags": ["AI"], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "provider": { - "enum": ["openai", "anthropic", "gemini", "vercel-ai-gateway", "openrouter", "ollama"], - "type": "string" - }, - "model": { "type": "string", "minLength": 1 }, - "apiKey": { "type": "string", "minLength": 1 }, - "baseURL": { "type": "string", "default": "" } - }, - "required": ["provider", "model", "apiKey"] - } - } - } - }, - "responses": { - "200": { - "description": "The AI provider connection was successful.", - "content": { "application/json": { "schema": { "anyOf": [{}, { "not": {} }] } } } - }, - "400": { - "description": "400", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { "const": true }, - "code": { "const": "BAD_REQUEST" }, - "status": { "const": 400 }, - "message": { "type": "string", "default": "Invalid AI provider configuration." }, - "data": {} - }, - "required": ["defined", "code", "status", "message"] - }, - { - "type": "object", - "properties": { - "defined": { "const": false }, - "code": { "type": "string" }, - "status": { "type": "number" }, - "message": { "type": "string" }, - "data": {} - }, - "required": ["defined", "code", "status", "message"] - } - ] - } - } - } - }, - "502": { - "description": "502", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { "const": true }, - "code": { "const": "BAD_GATEWAY" }, - "status": { "const": 502 }, - "message": { - "type": "string", - "default": "The AI provider returned an error or is unreachable." - }, - "data": {} - }, - "required": ["defined", "code", "status", "message"] - }, - { - "type": "object", - "properties": { - "defined": { "const": false }, - "code": { "type": "string" }, - "status": { "type": "number" }, - "message": { "type": "string" }, - "data": {} - }, - "required": ["defined", "code", "status", "message"] - } - ] - } - } - } - } - } - } - }, "/ai/parse-pdf": { "post": { "operationId": "parseResumePdf", @@ -1855,20 +2524,24 @@ "schema": { "type": "object", "properties": { - "provider": { - "enum": ["openai", "anthropic", "gemini", "vercel-ai-gateway", "openrouter", "ollama"], + "aiProviderId": { "type": "string" }, - "model": { "type": "string", "minLength": 1 }, - "apiKey": { "type": "string", "minLength": 1 }, - "baseURL": { "type": "string", "default": "" }, "file": { "type": "object", - "properties": { "name": { "type": "string" }, "data": { "type": "string", "maxLength": 13981018 } }, + "properties": { + "name": { + "type": "string" + }, + "data": { + "type": "string", + "maxLength": 13981018 + } + }, "required": ["name", "data"] } }, - "required": ["provider", "model", "apiKey", "file"] + "required": ["file"] } } } @@ -1876,7 +2549,18 @@ "responses": { "200": { "description": "The PDF was successfully parsed into structured resume data.", - "content": { "application/json": { "schema": { "anyOf": [{}, { "not": {} }] } } } + "content": { + "application/json": { + "schema": { + "anyOf": [ + {}, + { + "not": {} + } + ] + } + } + } }, "400": { "description": "400", @@ -1887,9 +2571,15 @@ { "type": "object", "properties": { - "defined": { "const": true }, - "code": { "const": "BAD_REQUEST" }, - "status": { "const": 400 }, + "defined": { + "const": true + }, + "code": { + "const": "BAD_REQUEST" + }, + "status": { + "const": 400 + }, "message": { "type": "string", "default": "The AI returned an improperly formatted structure." @@ -1901,10 +2591,18 @@ { "type": "object", "properties": { - "defined": { "const": false }, - "code": { "type": "string" }, - "status": { "type": "number" }, - "message": { "type": "string" }, + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, "data": {} }, "required": ["defined", "code", "status", "message"] @@ -1923,9 +2621,15 @@ { "type": "object", "properties": { - "defined": { "const": true }, - "code": { "const": "BAD_GATEWAY" }, - "status": { "const": 502 }, + "defined": { + "const": true + }, + "code": { + "const": "BAD_GATEWAY" + }, + "status": { + "const": 502 + }, "message": { "type": "string", "default": "The AI provider returned an error or is unreachable." @@ -1937,10 +2641,18 @@ { "type": "object", "properties": { - "defined": { "const": false }, - "code": { "type": "string" }, - "status": { "type": "number" }, - "message": { "type": "string" }, + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, "data": {} }, "required": ["defined", "code", "status", "message"] @@ -1966,16 +2678,20 @@ "schema": { "type": "object", "properties": { - "provider": { - "enum": ["openai", "anthropic", "gemini", "vercel-ai-gateway", "openrouter", "ollama"], + "aiProviderId": { "type": "string" }, - "model": { "type": "string", "minLength": 1 }, - "apiKey": { "type": "string", "minLength": 1 }, - "baseURL": { "type": "string", "default": "" }, "file": { "type": "object", - "properties": { "name": { "type": "string" }, "data": { "type": "string", "maxLength": 13981018 } }, + "properties": { + "name": { + "type": "string" + }, + "data": { + "type": "string", + "maxLength": 13981018 + } + }, "required": ["name", "data"] }, "mediaType": { @@ -1986,7 +2702,7 @@ "type": "string" } }, - "required": ["provider", "model", "apiKey", "file", "mediaType"] + "required": ["file", "mediaType"] } } } @@ -1994,7 +2710,18 @@ "responses": { "200": { "description": "The DOCX was successfully parsed into structured resume data.", - "content": { "application/json": { "schema": { "anyOf": [{}, { "not": {} }] } } } + "content": { + "application/json": { + "schema": { + "anyOf": [ + {}, + { + "not": {} + } + ] + } + } + } }, "400": { "description": "400", @@ -2005,9 +2732,15 @@ { "type": "object", "properties": { - "defined": { "const": true }, - "code": { "const": "BAD_REQUEST" }, - "status": { "const": 400 }, + "defined": { + "const": true + }, + "code": { + "const": "BAD_REQUEST" + }, + "status": { + "const": 400 + }, "message": { "type": "string", "default": "The AI returned an improperly formatted structure." @@ -2019,10 +2752,18 @@ { "type": "object", "properties": { - "defined": { "const": false }, - "code": { "type": "string" }, - "status": { "type": "number" }, - "message": { "type": "string" }, + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, "data": {} }, "required": ["defined", "code", "status", "message"] @@ -2041,9 +2782,15 @@ { "type": "object", "properties": { - "defined": { "const": true }, - "code": { "const": "BAD_GATEWAY" }, - "status": { "const": 502 }, + "defined": { + "const": true + }, + "code": { + "const": "BAD_GATEWAY" + }, + "status": { + "const": 502 + }, "message": { "type": "string", "default": "The AI provider returned an error or is unreachable." @@ -2055,10 +2802,18 @@ { "type": "object", "properties": { - "defined": { "const": false }, - "code": { "type": "string" }, - "status": { "type": "number" }, - "message": { "type": "string" }, + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, "data": {} }, "required": ["defined", "code", "status", "message"] @@ -2075,12 +2830,23 @@ "post": { "operationId": "aiChat", "summary": "Chat with AI to modify resume", - "description": "Streams a chat response from the configured AI provider. The LLM can call the patch_resume tool to generate JSON Patch operations that modify the resume. Requires authentication and AI provider credentials.", + "description": "Streams a chat response from the configured AI provider. The LLM can call the propose_resume_patches tool to generate JSON Patch proposals for explicit user approval. Requires authentication and AI provider credentials.", "tags": ["AI"], "responses": { "200": { "description": "OK", - "content": { "application/json": { "schema": { "anyOf": [{}, { "not": {} }] } } } + "content": { + "application/json": { + "schema": { + "anyOf": [ + {}, + { + "not": {} + } + ] + } + } + } } } } @@ -2098,17 +2864,14 @@ "schema": { "type": "object", "properties": { - "provider": { - "enum": ["openai", "anthropic", "gemini", "vercel-ai-gateway", "openrouter", "ollama"], + "aiProviderId": { "type": "string" }, - "model": { "type": "string", "minLength": 1 }, - "apiKey": { "type": "string", "minLength": 1 }, - "baseURL": { "type": "string", "default": "" }, - "resumeId": { "type": "string" }, - "resumeData": { "$ref": "#/components/schemas/ResumeData" } + "resumeId": { + "type": "string" + } }, - "required": ["provider", "model", "apiKey", "resumeId", "resumeData"] + "required": ["resumeId"] } } } @@ -2121,16 +2884,30 @@ "schema": { "type": "object", "properties": { - "overallScore": { "type": "integer", "minimum": 0, "maximum": 100 }, + "overallScore": { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, "scorecard": { "type": "array", "minItems": 1, "items": { "type": "object", "properties": { - "dimension": { "type": "string", "minLength": 1 }, - "score": { "type": "integer", "minimum": 0, "maximum": 100 }, - "rationale": { "type": "string", "minLength": 1 } + "dimension": { + "type": "string", + "minLength": 1 + }, + "score": { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + "rationale": { + "type": "string", + "minLength": 1 + } }, "required": ["dimension", "score", "rationale"] } @@ -2141,22 +2918,60 @@ "items": { "type": "object", "properties": { - "title": { "type": "string", "minLength": 1 }, - "impact": { "enum": ["high", "medium", "low"], "type": "string" }, - "why": { "type": "string", "minLength": 1 }, - "exampleRewrite": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "copyPrompt": { "type": "string", "minLength": 1 } + "title": { + "type": "string", + "minLength": 1 + }, + "impact": { + "enum": ["high", "medium", "low"], + "type": "string" + }, + "why": { + "type": "string", + "minLength": 1 + }, + "exampleRewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "copyPrompt": { + "type": "string", + "minLength": 1 + } }, "required": ["title", "impact", "why", "exampleRewrite", "copyPrompt"] } }, - "strengths": { "type": "array", "maxItems": 10, "items": { "type": "string", "minLength": 1 } }, - "updatedAt": { "type": "string", "format": "date-time", "x-native-type": "date" }, + "strengths": { + "type": "array", + "maxItems": 10, + "items": { + "type": "string", + "minLength": 1 + } + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, "modelMeta": { "type": "object", "properties": { - "provider": { "type": "string", "minLength": 1 }, - "model": { "type": "string", "minLength": 1 } + "provider": { + "type": "string", + "minLength": 1 + }, + "model": { + "type": "string", + "minLength": 1 + } }, "required": ["provider", "model"] } @@ -2175,9 +2990,15 @@ { "type": "object", "properties": { - "defined": { "const": true }, - "code": { "const": "BAD_REQUEST" }, - "status": { "const": 400 }, + "defined": { + "const": true + }, + "code": { + "const": "BAD_REQUEST" + }, + "status": { + "const": 400 + }, "message": { "type": "string", "default": "The AI returned an improperly formatted structure." @@ -2189,10 +3010,18 @@ { "type": "object", "properties": { - "defined": { "const": false }, - "code": { "type": "string" }, - "status": { "type": "number" }, - "message": { "type": "string" }, + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, "data": {} }, "required": ["defined", "code", "status", "message"] @@ -2211,9 +3040,15 @@ { "type": "object", "properties": { - "defined": { "const": true }, - "code": { "const": "BAD_GATEWAY" }, - "status": { "const": 502 }, + "defined": { + "const": true + }, + "code": { + "const": "BAD_GATEWAY" + }, + "status": { + "const": 502 + }, "message": { "type": "string", "default": "The AI provider returned an error or is unreachable." @@ -2225,10 +3060,18 @@ { "type": "object", "properties": { - "defined": { "const": false }, - "code": { "type": "string" }, - "status": { "type": "number" }, - "message": { "type": "string" }, + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, "data": {} }, "required": ["defined", "code", "status", "message"] @@ -2241,6 +3084,4216 @@ } } }, + "/ai-providers": { + "get": { + "operationId": "listAiProviders", + "summary": "List saved AI providers", + "description": "Lists saved provider/model/API key combinations for the authenticated user. API keys are redacted.", + "tags": ["AI Providers"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "anyOf": [ + {}, + { + "not": {} + } + ] + } + } + } + }, + "412": { + "description": "412", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "PRECONDITION_FAILED" + }, + "status": { + "const": 412 + }, + "message": { + "type": "string", + "default": "AI agent workspace is not configured." + }, + "data": {} + }, + "required": ["defined", "code", "status", "message"] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": ["defined", "code", "status", "message"] + } + ] + } + } + } + } + } + }, + "post": { + "operationId": "createAiProvider", + "summary": "Create saved AI provider", + "description": "Stores an encrypted provider/model/API key combination. The key is never returned.", + "tags": ["AI Providers"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1 + }, + "provider": { + "enum": [ + "openai", + "anthropic", + "gemini", + "vercel-ai-gateway", + "openrouter", + "ollama", + "openai-compatible" + ], + "type": "string" + }, + "model": { + "type": "string", + "minLength": 1 + }, + "baseURL": { + "type": "string", + "default": "" + }, + "apiKey": { + "type": "string", + "minLength": 1 + } + }, + "required": ["label", "provider", "model", "apiKey"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "anyOf": [ + {}, + { + "not": {} + } + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "BAD_REQUEST" + }, + "status": { + "const": 400 + }, + "message": { + "type": "string", + "default": "Invalid AI provider configuration." + }, + "data": {} + }, + "required": ["defined", "code", "status", "message"] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": ["defined", "code", "status", "message"] + } + ] + } + } + } + }, + "412": { + "description": "412", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "PRECONDITION_FAILED" + }, + "status": { + "const": 412 + }, + "message": { + "type": "string", + "default": "AI agent workspace is not configured." + }, + "data": {} + }, + "required": ["defined", "code", "status", "message"] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": ["defined", "code", "status", "message"] + } + ] + } + } + } + } + } + } + }, + "/ai-providers/{id}": { + "patch": { + "operationId": "updateAiProvider", + "summary": "Update saved AI provider", + "description": "Updates a saved provider/model/API key combination. Updating the key requires retesting before use.", + "tags": ["AI Providers"], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1 + }, + "provider": { + "enum": [ + "openai", + "anthropic", + "gemini", + "vercel-ai-gateway", + "openrouter", + "ollama", + "openai-compatible" + ], + "type": "string" + }, + "model": { + "type": "string", + "minLength": 1 + }, + "baseURL": { + "type": "string", + "default": "" + }, + "apiKey": { + "type": "string", + "minLength": 1 + }, + "enabled": { + "type": "boolean" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "anyOf": [ + {}, + { + "not": {} + } + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "BAD_REQUEST" + }, + "status": { + "const": 400 + }, + "message": { + "type": "string", + "default": "Invalid AI provider configuration." + }, + "data": {} + }, + "required": ["defined", "code", "status", "message"] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": ["defined", "code", "status", "message"] + } + ] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "NOT_FOUND" + }, + "status": { + "const": 404 + }, + "message": { + "type": "string", + "default": "AI provider was not found." + }, + "data": {} + }, + "required": ["defined", "code", "status", "message"] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": ["defined", "code", "status", "message"] + } + ] + } + } + } + }, + "412": { + "description": "412", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "PRECONDITION_FAILED" + }, + "status": { + "const": 412 + }, + "message": { + "type": "string", + "default": "AI agent workspace is not configured." + }, + "data": {} + }, + "required": ["defined", "code", "status", "message"] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": ["defined", "code", "status", "message"] + } + ] + } + } + } + } + } + }, + "delete": { + "operationId": "deleteAiProvider", + "summary": "Delete saved AI provider", + "description": "Deletes a saved provider/model/API key combination.", + "tags": ["AI Providers"], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "not": {} + }, + { + "not": {} + } + ] + } + } + } + }, + "412": { + "description": "412", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "PRECONDITION_FAILED" + }, + "status": { + "const": 412 + }, + "message": { + "type": "string", + "default": "AI agent workspace is not configured." + }, + "data": {} + }, + "required": ["defined", "code", "status", "message"] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": ["defined", "code", "status", "message"] + } + ] + } + } + } + } + } + } + }, + "/ai-providers/{id}/test": { + "post": { + "operationId": "testAiProvider", + "summary": "Test saved AI provider", + "description": "Decrypts the saved API key server-side and validates the provider/model connection.", + "tags": ["AI Providers"], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "anyOf": [ + {}, + { + "not": {} + } + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "BAD_REQUEST" + }, + "status": { + "const": 400 + }, + "message": { + "type": "string", + "default": "Invalid AI provider configuration." + }, + "data": {} + }, + "required": ["defined", "code", "status", "message"] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": ["defined", "code", "status", "message"] + } + ] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "NOT_FOUND" + }, + "status": { + "const": 404 + }, + "message": { + "type": "string", + "default": "AI provider was not found." + }, + "data": {} + }, + "required": ["defined", "code", "status", "message"] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": ["defined", "code", "status", "message"] + } + ] + } + } + } + }, + "412": { + "description": "412", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "PRECONDITION_FAILED" + }, + "status": { + "const": 412 + }, + "message": { + "type": "string", + "default": "AI agent workspace is not configured." + }, + "data": {} + }, + "required": ["defined", "code", "status", "message"] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": ["defined", "code", "status", "message"] + } + ] + } + } + } + }, + "502": { + "description": "502", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "BAD_GATEWAY" + }, + "status": { + "const": 502 + }, + "message": { + "type": "string", + "default": "The AI provider returned an error or is unreachable." + }, + "data": {} + }, + "required": ["defined", "code", "status", "message"] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": ["defined", "code", "status", "message"] + } + ] + } + } + } + } + } + } + }, + "/agent/threads": { + "get": { + "operationId": "listAgentThreads", + "summary": "List agent threads", + "tags": ["Agent"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "anyOf": [ + {}, + { + "not": {} + } + ] + } + } + } + } + } + }, + "post": { + "operationId": "createAgentThread", + "summary": "Create agent thread", + "tags": ["Agent"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "aiProviderId": { + "type": "string" + }, + "sourceResumeId": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "anyOf": [ + {}, + { + "not": {} + } + ] + } + } + } + } + } + } + }, + "/agent/threads/for-resume": { + "post": { + "operationId": "getOrCreateAgentThreadForResume", + "summary": "Get or create an in-resume agent thread", + "tags": ["Agent"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "resumeId": { + "type": "string" + }, + "aiProviderId": { + "type": "string" + } + }, + "required": ["resumeId"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "anyOf": [ + {}, + { + "not": {} + } + ] + } + } + } + } + } + } + }, + "/agent/threads/{id}": { + "get": { + "operationId": "getAgentThread", + "summary": "Get agent thread", + "tags": ["Agent"], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "anyOf": [ + {}, + { + "not": {} + } + ] + } + } + } + } + } + }, + "delete": { + "operationId": "deleteAgentThread", + "summary": "Delete agent thread", + "tags": ["Agent"], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "not": {} + }, + { + "not": {} + } + ] + } + } + } + } + } + } + }, + "/agent/threads/{id}/archive": { + "post": { + "operationId": "archiveAgentThread", + "summary": "Archive agent thread", + "tags": ["Agent"], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "not": {} + }, + { + "not": {} + } + ] + } + } + } + } + } + } + }, + "/agent/messages/send": { + "post": { + "operationId": "sendAgentMessage", + "summary": "Send agent message", + "tags": ["Agent"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "threadId": { + "type": "string" + }, + "message": { + "not": {} + }, + "attachmentIds": { + "type": "array", + "maxItems": 10, + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "required": ["threadId", "message"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "anyOf": [ + {}, + { + "not": {} + } + ] + } + } + } + } + } + } + }, + "/agent/messages/stop": { + "post": { + "operationId": "stopAgentMessage", + "summary": "Stop active agent run", + "tags": ["Agent"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "threadId": { + "type": "string" + }, + "partialMessage": { + "not": {} + } + }, + "required": ["threadId"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "not": {} + }, + { + "not": {} + } + ] + } + } + } + } + } + } + }, + "/agent/messages/resume": { + "get": { + "operationId": "resumeAgentMessages", + "summary": "Resume agent message stream", + "tags": ["Agent"], + "parameters": [ + { + "name": "threadId", + "in": "query", + "required": true, + "schema": { + "type": "string" + }, + "allowEmptyValue": true, + "allowReserved": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "anyOf": [ + {}, + { + "not": {} + } + ] + } + } + } + } + } + } + }, + "/agent/attachments": { + "post": { + "operationId": "createAgentAttachment", + "summary": "Create agent attachment", + "tags": ["Agent"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "threadId": { + "type": "string" + }, + "filename": { + "type": "string", + "minLength": 1 + }, + "mediaType": { + "type": "string", + "minLength": 1 + }, + "data": { + "type": "string", + "minLength": 1 + } + }, + "required": ["threadId", "filename", "mediaType", "data"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "anyOf": [ + {}, + { + "not": {} + } + ] + } + } + } + } + } + } + }, + "/agent/attachments/{id}": { + "delete": { + "operationId": "deleteAgentAttachment", + "summary": "Delete agent attachment", + "tags": ["Agent"], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "not": {} + }, + { + "not": {} + } + ] + } + } + } + } + } + } + }, + "/agent/actions/{id}/revert": { + "post": { + "operationId": "revertAgentAction", + "summary": "Restore agent action snapshot", + "tags": ["Agent"], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "anyOf": [ + {}, + { + "not": {} + } + ] + } + } + } + } + } + } + }, + "/applications": { + "get": { + "operationId": "listApplications", + "summary": "List job applications", + "description": "Returns all job applications belonging to the authenticated user, most recently updated first. Archived applications are excluded unless includeArchived is set. Optionally filter by pipeline stage. Requires authentication.", + "tags": ["Applications"], + "parameters": [ + { + "name": "status", + "in": "query", + "schema": { + "enum": ["saved", "applied", "screening", "interview", "offer", "rejected"], + "type": "string" + }, + "allowEmptyValue": true, + "allowReserved": true + }, + { + "name": "tags", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "style": "deepObject", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true + }, + { + "name": "includeArchived", + "in": "query", + "schema": { + "type": "boolean", + "default": false + }, + "allowEmptyValue": true, + "allowReserved": true + } + ], + "responses": { + "200": { + "description": "A list of the user's job applications.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the application." + }, + "company": { + "type": "string", + "minLength": 1, + "description": "The company applied to." + }, + "role": { + "type": "string", + "minLength": 1, + "description": "The role / job title." + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "salary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "status": { + "enum": ["saved", "applied", "screening", "interview", "offer", "rejected"], + "type": "string", + "description": "The current pipeline stage." + }, + "archived": { + "type": "boolean" + }, + "resumeId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The linked Reactive Resume, if any." + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "sourceUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "jobDescription": { + "anyOf": [ + { + "type": "string", + "maxLength": 20000 + }, + { + "type": "null" + } + ] + }, + "matchScore": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + { + "type": "null" + } + ] + }, + "aiMetadata": { + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ] + }, + "notes": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "resumeFileUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "resumeFileName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "coverLetterUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "coverLetterName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "followUpAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + { + "type": "null" + } + ] + }, + "followUpNote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "contacts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "role": { + "type": "string", + "default": "" + }, + "type": { + "type": "string", + "default": "" + } + }, + "required": ["name"] + } + }, + "activity": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "type": { + "enum": ["stage", "note", "created"], + "type": "string" + }, + "text": { + "type": "string", + "minLength": 1 + }, + "at": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + } + }, + "required": ["id", "type", "text", "at"] + } + }, + "appliedAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + } + }, + "required": [ + "id", + "company", + "role", + "location", + "salary", + "status", + "archived", + "resumeId", + "source", + "tags", + "sourceUrl", + "jobDescription", + "matchScore", + "aiMetadata", + "notes", + "resumeFileUrl", + "resumeFileName", + "coverLetterUrl", + "coverLetterName", + "followUpAt", + "followUpNote", + "contacts", + "activity", + "appliedAt", + "createdAt", + "updatedAt" + ] + } + } + } + } + } + } + }, + "post": { + "operationId": "createApplication", + "summary": "Create a job application", + "description": "Creates a new job application in the pipeline. Company and role are required; all other fields (stage, location, salary, source, linked resume, follow-up, notes, contacts) are optional. Requires authentication.", + "tags": ["Applications"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "company": { + "type": "string", + "minLength": 1, + "description": "The company applied to." + }, + "role": { + "type": "string", + "minLength": 1, + "description": "The role / job title." + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "salary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "sourceUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "jobDescription": { + "anyOf": [ + { + "type": "string", + "maxLength": 20000 + }, + { + "type": "null" + } + ] + }, + "notes": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "resumeFileUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "resumeFileName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "coverLetterUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "coverLetterName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "followUpAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + { + "type": "null" + } + ] + }, + "followUpNote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "contacts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "role": { + "type": "string", + "default": "" + }, + "type": { + "type": "string", + "default": "" + } + }, + "required": ["name"] + } + }, + "resumeId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The linked Reactive Resume, if any." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "enum": ["saved", "applied", "screening", "interview", "offer", "rejected"], + "type": "string" + } + }, + "required": ["company", "role"] + } + } + } + }, + "responses": { + "200": { + "description": "The ID of the newly created application.", + "content": { + "application/json": { + "schema": { + "type": "string", + "description": "The ID of the created application." + } + } + } + } + } + } + }, + "/applications/{id}": { + "get": { + "operationId": "getApplication", + "summary": "Get application by ID", + "description": "Returns a single job application with its full detail (contacts, activity timeline, linked resume). Only applications belonging to the authenticated user can be retrieved. Requires authentication.", + "tags": ["Applications"], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The ID of the application." + } + } + ], + "responses": { + "200": { + "description": "The job application.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the application." + }, + "company": { + "type": "string", + "minLength": 1, + "description": "The company applied to." + }, + "role": { + "type": "string", + "minLength": 1, + "description": "The role / job title." + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "salary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "status": { + "enum": ["saved", "applied", "screening", "interview", "offer", "rejected"], + "type": "string", + "description": "The current pipeline stage." + }, + "archived": { + "type": "boolean" + }, + "resumeId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The linked Reactive Resume, if any." + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "sourceUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "jobDescription": { + "anyOf": [ + { + "type": "string", + "maxLength": 20000 + }, + { + "type": "null" + } + ] + }, + "matchScore": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + { + "type": "null" + } + ] + }, + "aiMetadata": { + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ] + }, + "notes": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "resumeFileUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "resumeFileName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "coverLetterUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "coverLetterName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "followUpAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + { + "type": "null" + } + ] + }, + "followUpNote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "contacts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "role": { + "type": "string", + "default": "" + }, + "type": { + "type": "string", + "default": "" + } + }, + "required": ["name"] + } + }, + "activity": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "type": { + "enum": ["stage", "note", "created"], + "type": "string" + }, + "text": { + "type": "string", + "minLength": 1 + }, + "at": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + } + }, + "required": ["id", "type", "text", "at"] + } + }, + "appliedAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + } + }, + "required": [ + "id", + "company", + "role", + "location", + "salary", + "status", + "archived", + "resumeId", + "source", + "tags", + "sourceUrl", + "jobDescription", + "matchScore", + "aiMetadata", + "notes", + "resumeFileUrl", + "resumeFileName", + "coverLetterUrl", + "coverLetterName", + "followUpAt", + "followUpNote", + "contacts", + "activity", + "appliedAt", + "createdAt", + "updatedAt" + ] + } + } + } + } + } + }, + "put": { + "operationId": "updateApplication", + "summary": "Update a job application", + "description": "Updates one or more fields of an application, including moving it to a different pipeline stage or archiving it. Moving stages automatically appends an entry to the activity timeline. Only provided fields are changed. Requires authentication.", + "tags": ["Applications"], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "company": { + "type": "string", + "minLength": 1, + "description": "The company applied to." + }, + "role": { + "type": "string", + "minLength": 1, + "description": "The role / job title." + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "salary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "sourceUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "jobDescription": { + "anyOf": [ + { + "type": "string", + "maxLength": 20000 + }, + { + "type": "null" + } + ] + }, + "notes": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "resumeFileUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "resumeFileName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "coverLetterUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "coverLetterName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "followUpAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + { + "type": "null" + } + ] + }, + "followUpNote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "contacts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "role": { + "type": "string", + "default": "" + }, + "type": { + "type": "string", + "default": "" + } + }, + "required": ["name"] + } + }, + "resumeId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The linked Reactive Resume, if any." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "enum": ["saved", "applied", "screening", "interview", "offer", "rejected"], + "type": "string" + }, + "archived": { + "type": "boolean" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "The updated application.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the application." + }, + "company": { + "type": "string", + "minLength": 1, + "description": "The company applied to." + }, + "role": { + "type": "string", + "minLength": 1, + "description": "The role / job title." + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "salary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "status": { + "enum": ["saved", "applied", "screening", "interview", "offer", "rejected"], + "type": "string", + "description": "The current pipeline stage." + }, + "archived": { + "type": "boolean" + }, + "resumeId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The linked Reactive Resume, if any." + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "sourceUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "jobDescription": { + "anyOf": [ + { + "type": "string", + "maxLength": 20000 + }, + { + "type": "null" + } + ] + }, + "matchScore": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + { + "type": "null" + } + ] + }, + "aiMetadata": { + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ] + }, + "notes": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "resumeFileUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "resumeFileName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "coverLetterUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "coverLetterName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "followUpAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + { + "type": "null" + } + ] + }, + "followUpNote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "contacts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "role": { + "type": "string", + "default": "" + }, + "type": { + "type": "string", + "default": "" + } + }, + "required": ["name"] + } + }, + "activity": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "type": { + "enum": ["stage", "note", "created"], + "type": "string" + }, + "text": { + "type": "string", + "minLength": 1 + }, + "at": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + } + }, + "required": ["id", "type", "text", "at"] + } + }, + "appliedAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + } + }, + "required": [ + "id", + "company", + "role", + "location", + "salary", + "status", + "archived", + "resumeId", + "source", + "tags", + "sourceUrl", + "jobDescription", + "matchScore", + "aiMetadata", + "notes", + "resumeFileUrl", + "resumeFileName", + "coverLetterUrl", + "coverLetterName", + "followUpAt", + "followUpNote", + "contacts", + "activity", + "appliedAt", + "createdAt", + "updatedAt" + ] + } + } + } + } + } + }, + "delete": { + "operationId": "deleteApplication", + "summary": "Delete a job application", + "description": "Permanently deletes a job application. Requires authentication.", + "tags": ["Applications"], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The ID of the application." + } + } + ], + "responses": { + "200": { + "description": "The application was deleted successfully.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "not": {} + }, + { + "not": {} + } + ] + } + } + } + } + } + } + }, + "/applications/import": { + "post": { + "operationId": "importApplications", + "summary": "Bulk import applications", + "description": "Creates many applications at once from a parsed CSV. Each item requires company and role. Returns the number imported. Requires authentication.", + "tags": ["Applications"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "minItems": 1, + "maxItems": 500, + "items": { + "type": "object", + "properties": { + "company": { + "type": "string", + "minLength": 1, + "description": "The company applied to." + }, + "role": { + "type": "string", + "minLength": 1, + "description": "The role / job title." + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "salary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "sourceUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "jobDescription": { + "anyOf": [ + { + "type": "string", + "maxLength": 20000 + }, + { + "type": "null" + } + ] + }, + "notes": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "resumeFileUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "resumeFileName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "coverLetterUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "coverLetterName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "followUpAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + { + "type": "null" + } + ] + }, + "followUpNote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "contacts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "role": { + "type": "string", + "default": "" + }, + "type": { + "type": "string", + "default": "" + } + }, + "required": ["name"] + } + }, + "resumeId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The linked Reactive Resume, if any." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "enum": ["saved", "applied", "screening", "interview", "offer", "rejected"], + "type": "string" + } + }, + "required": ["company", "role"] + } + } + }, + "required": ["items"] + } + } + } + }, + "responses": { + "200": { + "description": "The number of applications imported.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "imported": { + "type": "number" + } + }, + "required": ["imported"] + } + } + } + } + } + } + }, + "/applications/{id}/documents/{kind}": { + "post": { + "operationId": "attachApplicationDocument", + "summary": "Attach an application document", + "description": "Uploads and attaches a PDF document to an application. Kind must be either resume or cover-letter. Requires authentication.", + "tags": ["Applications"], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "kind", + "in": "path", + "required": true, + "schema": { + "enum": ["resume", "cover-letter"], + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "contentMediaType": "application/pdf" + } + }, + "required": ["file"] + } + } + } + }, + "responses": { + "200": { + "description": "The updated application.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the application." + }, + "company": { + "type": "string", + "minLength": 1, + "description": "The company applied to." + }, + "role": { + "type": "string", + "minLength": 1, + "description": "The role / job title." + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "salary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "status": { + "enum": ["saved", "applied", "screening", "interview", "offer", "rejected"], + "type": "string", + "description": "The current pipeline stage." + }, + "archived": { + "type": "boolean" + }, + "resumeId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The linked Reactive Resume, if any." + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "sourceUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "jobDescription": { + "anyOf": [ + { + "type": "string", + "maxLength": 20000 + }, + { + "type": "null" + } + ] + }, + "matchScore": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + { + "type": "null" + } + ] + }, + "aiMetadata": { + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ] + }, + "notes": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "resumeFileUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "resumeFileName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "coverLetterUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "coverLetterName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "followUpAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + { + "type": "null" + } + ] + }, + "followUpNote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "contacts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "role": { + "type": "string", + "default": "" + }, + "type": { + "type": "string", + "default": "" + } + }, + "required": ["name"] + } + }, + "activity": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "type": { + "enum": ["stage", "note", "created"], + "type": "string" + }, + "text": { + "type": "string", + "minLength": 1 + }, + "at": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + } + }, + "required": ["id", "type", "text", "at"] + } + }, + "appliedAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + } + }, + "required": [ + "id", + "company", + "role", + "location", + "salary", + "status", + "archived", + "resumeId", + "source", + "tags", + "sourceUrl", + "jobDescription", + "matchScore", + "aiMetadata", + "notes", + "resumeFileUrl", + "resumeFileName", + "coverLetterUrl", + "coverLetterName", + "followUpAt", + "followUpNote", + "contacts", + "activity", + "appliedAt", + "createdAt", + "updatedAt" + ] + } + } + } + } + } + }, + "delete": { + "operationId": "removeApplicationDocument", + "summary": "Remove an application document", + "description": "Removes a resume or cover-letter PDF from an application and clears the stored document fields. Requires authentication.", + "tags": ["Applications"], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "kind", + "in": "path", + "required": true, + "schema": { + "enum": ["resume", "cover-letter"], + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The updated application.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the application." + }, + "company": { + "type": "string", + "minLength": 1, + "description": "The company applied to." + }, + "role": { + "type": "string", + "minLength": 1, + "description": "The role / job title." + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "salary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "status": { + "enum": ["saved", "applied", "screening", "interview", "offer", "rejected"], + "type": "string", + "description": "The current pipeline stage." + }, + "archived": { + "type": "boolean" + }, + "resumeId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The linked Reactive Resume, if any." + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "sourceUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "jobDescription": { + "anyOf": [ + { + "type": "string", + "maxLength": 20000 + }, + { + "type": "null" + } + ] + }, + "matchScore": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + { + "type": "null" + } + ] + }, + "aiMetadata": { + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ] + }, + "notes": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "resumeFileUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "resumeFileName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "coverLetterUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "coverLetterName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "followUpAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + { + "type": "null" + } + ] + }, + "followUpNote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "contacts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "role": { + "type": "string", + "default": "" + }, + "type": { + "type": "string", + "default": "" + } + }, + "required": ["name"] + } + }, + "activity": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "type": { + "enum": ["stage", "note", "created"], + "type": "string" + }, + "text": { + "type": "string", + "minLength": 1 + }, + "at": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + } + }, + "required": ["id", "type", "text", "at"] + } + }, + "appliedAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + } + }, + "required": [ + "id", + "company", + "role", + "location", + "salary", + "status", + "archived", + "resumeId", + "source", + "tags", + "sourceUrl", + "jobDescription", + "matchScore", + "aiMetadata", + "notes", + "resumeFileUrl", + "resumeFileName", + "coverLetterUrl", + "coverLetterName", + "followUpAt", + "followUpNote", + "contacts", + "activity", + "appliedAt", + "createdAt", + "updatedAt" + ] + } + } + } + } + } + } + }, + "/applications/{id}/notes": { + "post": { + "operationId": "addApplicationNote", + "summary": "Log a note on the timeline", + "description": "Appends a free-text note to the application's activity timeline. Requires authentication.", + "tags": ["Applications"], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "text": { + "type": "string", + "minLength": 1 + } + }, + "required": ["text"] + } + } + } + }, + "responses": { + "200": { + "description": "The updated application.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the application." + }, + "company": { + "type": "string", + "minLength": 1, + "description": "The company applied to." + }, + "role": { + "type": "string", + "minLength": 1, + "description": "The role / job title." + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "salary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "status": { + "enum": ["saved", "applied", "screening", "interview", "offer", "rejected"], + "type": "string", + "description": "The current pipeline stage." + }, + "archived": { + "type": "boolean" + }, + "resumeId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The linked Reactive Resume, if any." + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "sourceUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "jobDescription": { + "anyOf": [ + { + "type": "string", + "maxLength": 20000 + }, + { + "type": "null" + } + ] + }, + "matchScore": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + { + "type": "null" + } + ] + }, + "aiMetadata": { + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ] + }, + "notes": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "resumeFileUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "resumeFileName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "coverLetterUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "coverLetterName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "followUpAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + { + "type": "null" + } + ] + }, + "followUpNote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "contacts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "role": { + "type": "string", + "default": "" + }, + "type": { + "type": "string", + "default": "" + } + }, + "required": ["name"] + } + }, + "activity": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "type": { + "enum": ["stage", "note", "created"], + "type": "string" + }, + "text": { + "type": "string", + "minLength": 1 + }, + "at": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + } + }, + "required": ["id", "type", "text", "at"] + } + }, + "appliedAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + } + }, + "required": [ + "id", + "company", + "role", + "location", + "salary", + "status", + "archived", + "resumeId", + "source", + "tags", + "sourceUrl", + "jobDescription", + "matchScore", + "aiMetadata", + "notes", + "resumeFileUrl", + "resumeFileName", + "coverLetterUrl", + "coverLetterName", + "followUpAt", + "followUpNote", + "contacts", + "activity", + "appliedAt", + "createdAt", + "updatedAt" + ] + } + } + } + } + } + } + }, + "/applications/bulk-update": { + "post": { + "operationId": "bulkUpdateApplications", + "summary": "Bulk update applications", + "description": "Applies the same change (move stage, archive/unarchive, add tags) to multiple applications at once. Requires authentication.", + "tags": ["Applications"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + }, + "status": { + "enum": ["saved", "applied", "screening", "interview", "offer", "rejected"], + "type": "string" + }, + "archived": { + "type": "boolean" + }, + "addTags": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["ids"] + } + } + } + }, + "responses": { + "200": { + "description": "The number of applications updated.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "updated": { + "type": "number" + } + }, + "required": ["updated"] + } + } + } + } + } + } + }, + "/applications/bulk-delete": { + "post": { + "operationId": "bulkDeleteApplications", + "summary": "Bulk delete applications", + "description": "Permanently deletes multiple applications at once. Requires authentication.", + "tags": ["Applications"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + }, + "required": ["ids"] + } + } + } + }, + "responses": { + "200": { + "description": "The number of applications deleted.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "deleted": { + "type": "number" + } + }, + "required": ["deleted"] + } + } + } + } + } + } + }, + "/applications/stats": { + "get": { + "operationId": "getApplicationStats", + "summary": "Application pipeline stats", + "description": "Returns aggregate counts (per stage, per source) for the Insights view. Requires authentication.", + "tags": ["Applications"], + "parameters": [], + "responses": { + "200": { + "description": "Aggregate application counts.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "total": { + "type": "number" + }, + "byStage": { + "type": "array", + "items": { + "type": "object", + "properties": { + "status": { + "enum": ["saved", "applied", "screening", "interview", "offer", "rejected"], + "type": "string" + }, + "count": { + "type": "number" + } + }, + "required": ["status", "count"] + } + }, + "bySource": { + "type": "array", + "items": { + "type": "object", + "properties": { + "source": { + "type": "string" + }, + "count": { + "type": "number" + } + }, + "required": ["source", "count"] + } + } + }, + "required": ["total", "byStage", "bySource"] + } + } + } + } + } + } + }, + "/applications/tags": { + "get": { + "operationId": "listApplicationTags", + "summary": "List application tags", + "description": "Returns the distinct tags used across the user's applications. Requires authentication.", + "tags": ["Applications"], + "responses": { + "200": { + "description": "Distinct tags.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + }, + "/applications/ai/autofill": { + "post": { + "operationId": "aiAutofillApplication", + "tags": ["Applications", "AI"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sourceUrl": { + "type": "string" + }, + "jobDescription": { + "type": "string", + "maxLength": 20000 + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "company": { + "type": "string" + }, + "role": { + "type": "string" + }, + "location": { + "type": "string" + }, + "salary": { + "type": "string" + }, + "jobDescription": { + "type": "string" + } + }, + "required": ["company", "role", "location", "salary", "jobDescription"] + } + } + } + } + } + } + }, + "/applications/{id}/ai/match-score": { + "post": { + "operationId": "aiApplicationMatchScore", + "tags": ["Applications", "AI"], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "score": {}, + "gaps": {}, + "strengths": {} + } + } + } + } + } + } + } + }, + "/applications/{id}/ai/draft-message": { + "post": { + "operationId": "aiDraftApplicationMessage", + "tags": ["Applications", "AI"], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "kind": { + "enum": ["cover-letter", "follow-up"], + "type": "string" + } + }, + "required": ["kind"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "text": { + "type": "string" + } + }, + "required": ["text"] + } + } + } + } + } + } + }, + "/applications/{id}/ai/tailor-resume": { + "post": { + "operationId": "aiTailorResumeForApplication", + "tags": ["Applications", "AI"], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "resumeId": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": ["resumeId", "name"] + } + } + } + } + } + } + }, "/auth/providers": { "get": { "operationId": "listAuthProviders", @@ -2250,7 +7303,43 @@ "responses": { "200": { "description": "A map of enabled authentication provider identifiers to their display names.", - "content": { "application/json": { "schema": { "anyOf": [{}, { "not": {} }] } } } + "content": { + "application/json": { + "schema": { + "anyOf": [ + {}, + { + "not": {} + } + ] + } + } + } + } + } + } + }, + "/auth/account/export": { + "get": { + "operationId": "exportAccountData", + "summary": "Export user account data", + "description": "Returns a JSON-serializable export of the authenticated user's data, including their public profile fields and all of their resumes. Secrets such as password hashes, tokens, and API keys are never included. Requires authentication.", + "tags": ["Authentication"], + "responses": { + "200": { + "description": "The user's exported account data.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + {}, + { + "not": {} + } + ] + } + } + } } } } @@ -2264,7 +7353,18 @@ "responses": { "200": { "description": "The user account and all associated data have been successfully deleted.", - "content": { "application/json": { "schema": { "anyOf": [{}, { "not": {} }] } } } + "content": { + "application/json": { + "schema": { + "anyOf": [ + {}, + { + "not": {} + } + ] + } + } + } } } } @@ -2294,9 +7394,13 @@ "showSponsors": { "type": "boolean", "description": "Whether sponsor placements are shown on this instance." + }, + "smtpEnabled": { + "type": "boolean", + "description": "Whether outbound email (SMTP) is configured on this instance." } }, - "required": ["disableSignups", "disableEmailAuth", "showSponsors"] + "required": ["disableSignups", "disableEmailAuth", "showSponsors", "smtpEnabled"] } } } @@ -2313,7 +7417,16 @@ "responses": { "200": { "description": "A sorted array of unique tag strings.", - "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string" } } } } + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } } } } @@ -2329,7 +7442,10 @@ "name": "id", "in": "path", "required": true, - "schema": { "type": "string", "description": "The unique identifier of the resume." } + "schema": { + "type": "string", + "description": "The unique identifier of the resume." + } } ], "responses": { @@ -2340,23 +7456,41 @@ "schema": { "type": "object", "properties": { - "isPublic": { "type": "boolean", "description": "Whether the resume is currently public." }, - "views": { "type": "number", "description": "Total number of times the resume has been viewed." }, + "isPublic": { + "type": "boolean", + "description": "Whether the resume is currently public." + }, + "views": { + "type": "number", + "description": "Total number of times the resume has been viewed." + }, "downloads": { "type": "number", "description": "Total number of times the resume has been downloaded." }, "lastViewedAt": { "anyOf": [ - { "type": "string", "format": "date-time", "x-native-type": "date" }, - { "type": "null" } + { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + { + "type": "null" + } ], "description": "Timestamp of the last view, or null if never viewed." }, "lastDownloadedAt": { "anyOf": [ - { "type": "string", "format": "date-time", "x-native-type": "date" }, - { "type": "null" } + { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + { + "type": "null" + } ], "description": "Timestamp of the last download, or null if never downloaded." } @@ -2369,6 +7503,68 @@ } } }, + "/resumes/{id}/statistics/daily": { + "get": { + "operationId": "getResumeDailyStatistics", + "summary": "Get resume daily statistics", + "description": "Returns a continuous, zero-filled per-day series of view and download counts for the specified resume over the last `days` days (UTC). Requires authentication and resume ownership.", + "tags": ["Resume Statistics"], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The unique identifier of the resume." + } + }, + { + "name": "days", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 365, + "default": 30, + "description": "Number of trailing days to include." + }, + "allowEmptyValue": true, + "allowReserved": true + } + ], + "responses": { + "200": { + "description": "The resume's daily view and download statistics.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "date": { + "type": "string", + "description": "The UTC day in YYYY-MM-DD format." + }, + "views": { + "type": "number", + "description": "Number of views recorded on this day." + }, + "downloads": { + "type": "number", + "description": "Number of downloads recorded on this day." + } + }, + "required": ["date", "views", "downloads"] + } + } + } + } + } + } + } + }, "/resumes/{id}/analysis": { "get": { "operationId": "getResumeAnalysis", @@ -2380,7 +7576,10 @@ "name": "id", "in": "path", "required": true, - "schema": { "type": "string", "description": "The unique identifier of the resume." } + "schema": { + "type": "string", + "description": "The unique identifier of the resume." + } } ], "responses": { @@ -2393,16 +7592,30 @@ { "type": "object", "properties": { - "overallScore": { "type": "integer", "minimum": 0, "maximum": 100 }, + "overallScore": { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, "scorecard": { "type": "array", "minItems": 1, "items": { "type": "object", "properties": { - "dimension": { "type": "string", "minLength": 1 }, - "score": { "type": "integer", "minimum": 0, "maximum": 100 }, - "rationale": { "type": "string", "minLength": 1 } + "dimension": { + "type": "string", + "minLength": 1 + }, + "score": { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + "rationale": { + "type": "string", + "minLength": 1 + } }, "required": ["dimension", "score", "rationale"] } @@ -2413,29 +7626,105 @@ "items": { "type": "object", "properties": { - "title": { "type": "string", "minLength": 1 }, - "impact": { "enum": ["high", "medium", "low"], "type": "string" }, - "why": { "type": "string", "minLength": 1 }, - "exampleRewrite": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "copyPrompt": { "type": "string", "minLength": 1 } + "title": { + "type": "string", + "minLength": 1 + }, + "impact": { + "enum": ["high", "medium", "low"], + "type": "string" + }, + "why": { + "type": "string", + "minLength": 1 + }, + "exampleRewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "copyPrompt": { + "type": "string", + "minLength": 1 + } }, "required": ["title", "impact", "why", "exampleRewrite", "copyPrompt"] } }, - "strengths": { "type": "array", "maxItems": 10, "items": { "type": "string", "minLength": 1 } }, - "updatedAt": { "type": "string", "format": "date-time", "x-native-type": "date" }, + "strengths": { + "type": "array", + "maxItems": 10, + "items": { + "type": "string", + "minLength": 1 + } + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, "modelMeta": { "type": "object", "properties": { - "provider": { "type": "string", "minLength": 1 }, - "model": { "type": "string", "minLength": 1 } + "provider": { + "type": "string", + "minLength": 1 + }, + "model": { + "type": "string", + "minLength": 1 + } }, "required": ["provider", "model"] } }, "required": ["overallScore", "scorecard", "suggestions", "strengths", "updatedAt", "modelMeta"] }, - { "type": "null" } + { + "type": "null" + } + ] + } + } + } + } + } + } + }, + "/resumes/{id}/updates": { + "get": { + "operationId": "subscribeResumeUpdates", + "summary": "Subscribe to resume updates", + "description": "Streams lightweight invalidation events when the specified resume changes. The event payload contains metadata only; clients should refetch the resume for canonical data.", + "tags": ["Resumes"], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The unique identifier of the resume." + } + } + ], + "responses": { + "200": { + "description": "A stream of resume update invalidation events.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + {}, + { + "not": {} + } ] } } @@ -2454,7 +7743,13 @@ { "name": "tags", "in": "query", - "schema": { "type": "array", "items": { "type": "string" }, "default": [] }, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, "style": "deepObject", "explode": true, "allowEmptyValue": true, @@ -2463,7 +7758,11 @@ { "name": "sort", "in": "query", - "schema": { "enum": ["lastUpdatedAt", "createdAt", "name"], "type": "string", "default": "lastUpdatedAt" }, + "schema": { + "enum": ["lastUpdatedAt", "createdAt", "name"], + "type": "string", + "default": "lastUpdatedAt" + }, "allowEmptyValue": true, "allowReserved": true } @@ -2478,16 +7777,35 @@ "items": { "type": "object", "properties": { - "id": { "type": "string", "description": "The ID of the resume." }, - "name": { "type": "string", "minLength": 1, "description": "The name of the resume." }, - "slug": { "type": "string", "minLength": 1, "description": "The slug of the resume." }, + "id": { + "type": "string", + "description": "The ID of the resume." + }, + "name": { + "type": "string", + "minLength": 1, + "description": "The name of the resume." + }, + "slug": { + "type": "string", + "minLength": 1, + "description": "The slug of the resume." + }, "tags": { "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "description": "The tags of the resume." }, - "isPublic": { "type": "boolean", "description": "Whether the resume is public." }, - "isLocked": { "type": "boolean", "description": "Whether the resume is locked." }, + "isPublic": { + "type": "boolean", + "description": "Whether the resume is public." + }, + "isLocked": { + "type": "boolean", + "description": "Whether the resume is locked." + }, "createdAt": { "type": "string", "format": "date-time", @@ -2521,10 +7839,27 @@ "schema": { "type": "object", "properties": { - "name": { "type": "string", "minLength": 1, "description": "The name of the resume." }, - "slug": { "type": "string", "minLength": 1, "description": "The slug of the resume." }, - "tags": { "type": "array", "items": { "type": "string" }, "description": "The tags of the resume." }, - "withSampleData": { "type": "boolean", "default": false } + "name": { + "type": "string", + "minLength": 1, + "description": "The name of the resume." + }, + "slug": { + "type": "string", + "minLength": 1, + "description": "The slug of the resume." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The tags of the resume." + }, + "withSampleData": { + "type": "boolean", + "default": false + } }, "required": ["name", "slug", "tags"] } @@ -2535,7 +7870,12 @@ "200": { "description": "The ID of the newly created resume.", "content": { - "application/json": { "schema": { "type": "string", "description": "The ID of the created resume." } } + "application/json": { + "schema": { + "type": "string", + "description": "The ID of the created resume." + } + } } }, "400": { @@ -2547,10 +7887,19 @@ { "type": "object", "properties": { - "defined": { "const": true }, - "code": { "const": "RESUME_SLUG_ALREADY_EXISTS" }, - "status": { "const": 400 }, - "message": { "type": "string", "default": "A resume with this slug already exists." }, + "defined": { + "const": true + }, + "code": { + "const": "RESUME_SLUG_ALREADY_EXISTS" + }, + "status": { + "const": 400 + }, + "message": { + "type": "string", + "default": "A resume with this slug already exists." + }, "data": {} }, "required": ["defined", "code", "status", "message"] @@ -2558,10 +7907,18 @@ { "type": "object", "properties": { - "defined": { "const": false }, - "code": { "type": "string" }, - "status": { "type": "number" }, - "message": { "type": "string" }, + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, "data": {} }, "required": ["defined", "code", "status", "message"] @@ -2585,7 +7942,10 @@ "name": "id", "in": "path", "required": true, - "schema": { "type": "string", "description": "The ID of the resume." } + "schema": { + "type": "string", + "description": "The ID of the resume." + } } ], "responses": { @@ -2596,20 +7956,49 @@ "schema": { "type": "object", "properties": { - "id": { "type": "string", "description": "The ID of the resume." }, - "name": { "type": "string", "minLength": 1, "description": "The name of the resume." }, - "slug": { "type": "string", "minLength": 1, "description": "The slug of the resume." }, + "id": { + "type": "string", + "description": "The ID of the resume." + }, + "name": { + "type": "string", + "minLength": 1, + "description": "The name of the resume." + }, + "slug": { + "type": "string", + "minLength": 1, + "description": "The slug of the resume." + }, "tags": { "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "description": "The tags of the resume." }, - "isPublic": { "type": "boolean", "description": "Whether the resume is public." }, - "isLocked": { "type": "boolean", "description": "Whether the resume is locked." }, - "data": { "$ref": "#/components/schemas/ResumeData" }, - "hasPassword": { "type": "boolean" } + "isPublic": { + "type": "boolean", + "description": "Whether the resume is public." + }, + "isLocked": { + "type": "boolean", + "description": "Whether the resume is locked." + }, + "data": { + "$ref": "#/components/schemas/ResumeData" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date", + "description": "The date and time the resume was last updated." + }, + "hasPassword": { + "type": "boolean" + } }, - "required": ["id", "name", "slug", "tags", "isPublic", "isLocked", "data", "hasPassword"] + "required": ["id", "name", "slug", "tags", "isPublic", "isLocked", "data", "updatedAt", "hasPassword"] } } } @@ -2621,7 +8010,16 @@ "summary": "Update a resume", "description": "Updates one or more fields of a resume identified by its ID. All fields are optional; only provided fields will be updated. Locked resumes cannot be updated. Requires authentication.", "tags": ["Resumes"], - "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], "requestBody": { "required": false, "content": { @@ -2629,11 +8027,30 @@ "schema": { "type": "object", "properties": { - "name": { "type": "string", "minLength": 1, "description": "The name of the resume." }, - "slug": { "type": "string", "minLength": 1, "description": "The slug of the resume." }, - "tags": { "type": "array", "items": { "type": "string" }, "description": "The tags of the resume." }, - "data": { "$ref": "#/components/schemas/ResumeData" }, - "isPublic": { "type": "boolean", "description": "Whether the resume is public." } + "name": { + "type": "string", + "minLength": 1, + "description": "The name of the resume." + }, + "slug": { + "type": "string", + "minLength": 1, + "description": "The slug of the resume." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The tags of the resume." + }, + "data": { + "$ref": "#/components/schemas/ResumeData" + }, + "isPublic": { + "type": "boolean", + "description": "Whether the resume is public." + } } } } @@ -2647,20 +8064,49 @@ "schema": { "type": "object", "properties": { - "id": { "type": "string", "description": "The ID of the resume." }, - "name": { "type": "string", "minLength": 1, "description": "The name of the resume." }, - "slug": { "type": "string", "minLength": 1, "description": "The slug of the resume." }, + "id": { + "type": "string", + "description": "The ID of the resume." + }, + "name": { + "type": "string", + "minLength": 1, + "description": "The name of the resume." + }, + "slug": { + "type": "string", + "minLength": 1, + "description": "The slug of the resume." + }, "tags": { "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "description": "The tags of the resume." }, - "isPublic": { "type": "boolean", "description": "Whether the resume is public." }, - "isLocked": { "type": "boolean", "description": "Whether the resume is locked." }, - "data": { "$ref": "#/components/schemas/ResumeData" }, - "hasPassword": { "type": "boolean" } + "isPublic": { + "type": "boolean", + "description": "Whether the resume is public." + }, + "isLocked": { + "type": "boolean", + "description": "Whether the resume is locked." + }, + "data": { + "$ref": "#/components/schemas/ResumeData" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date", + "description": "The date and time the resume was last updated." + }, + "hasPassword": { + "type": "boolean" + } }, - "required": ["id", "name", "slug", "tags", "isPublic", "isLocked", "data", "hasPassword"] + "required": ["id", "name", "slug", "tags", "isPublic", "isLocked", "data", "updatedAt", "hasPassword"] } } } @@ -2674,10 +8120,19 @@ { "type": "object", "properties": { - "defined": { "const": true }, - "code": { "const": "RESUME_SLUG_ALREADY_EXISTS" }, - "status": { "const": 400 }, - "message": { "type": "string", "default": "A resume with this slug already exists." }, + "defined": { + "const": true + }, + "code": { + "const": "RESUME_SLUG_ALREADY_EXISTS" + }, + "status": { + "const": 400 + }, + "message": { + "type": "string", + "default": "A resume with this slug already exists." + }, "data": {} }, "required": ["defined", "code", "status", "message"] @@ -2685,10 +8140,18 @@ { "type": "object", "properties": { - "defined": { "const": false }, - "code": { "type": "string" }, - "status": { "type": "number" }, - "message": { "type": "string" }, + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, "data": {} }, "required": ["defined", "code", "status", "message"] @@ -2710,7 +8173,10 @@ "name": "id", "in": "path", "required": true, - "schema": { "type": "string", "description": "The ID of the resume to patch." } + "schema": { + "type": "string", + "description": "The ID of the resume to patch." + } } ], "requestBody": { @@ -2720,6 +8186,12 @@ "schema": { "type": "object", "properties": { + "expectedUpdatedAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date", + "description": "If provided, the patch only applies when the resume version still matches this timestamp." + }, "operations": { "type": "array", "minItems": 1, @@ -2727,40 +8199,83 @@ "anyOf": [ { "type": "object", - "properties": { "op": { "const": "add" }, "path": { "type": "string" }, "value": {} }, - "required": ["op", "path"] - }, - { - "type": "object", - "properties": { "op": { "const": "remove" }, "path": { "type": "string" } }, - "required": ["op", "path"] - }, - { - "type": "object", - "properties": { "op": { "const": "replace" }, "path": { "type": "string" }, "value": {} }, + "properties": { + "op": { + "const": "add" + }, + "path": { + "type": "string" + }, + "value": {} + }, "required": ["op", "path"] }, { "type": "object", "properties": { - "op": { "const": "move" }, - "path": { "type": "string" }, - "from": { "type": "string" } + "op": { + "const": "remove" + }, + "path": { + "type": "string" + } + }, + "required": ["op", "path"] + }, + { + "type": "object", + "properties": { + "op": { + "const": "replace" + }, + "path": { + "type": "string" + }, + "value": {} + }, + "required": ["op", "path"] + }, + { + "type": "object", + "properties": { + "op": { + "const": "move" + }, + "path": { + "type": "string" + }, + "from": { + "type": "string" + } }, "required": ["op", "path", "from"] }, { "type": "object", "properties": { - "op": { "const": "copy" }, - "path": { "type": "string" }, - "from": { "type": "string" } + "op": { + "const": "copy" + }, + "path": { + "type": "string" + }, + "from": { + "type": "string" + } }, "required": ["op", "path", "from"] }, { "type": "object", - "properties": { "op": { "const": "test" }, "path": { "type": "string" }, "value": {} }, + "properties": { + "op": { + "const": "test" + }, + "path": { + "type": "string" + }, + "value": {} + }, "required": ["op", "path"] } ] @@ -2781,20 +8296,49 @@ "schema": { "type": "object", "properties": { - "id": { "type": "string", "description": "The ID of the resume." }, - "name": { "type": "string", "minLength": 1, "description": "The name of the resume." }, - "slug": { "type": "string", "minLength": 1, "description": "The slug of the resume." }, + "id": { + "type": "string", + "description": "The ID of the resume." + }, + "name": { + "type": "string", + "minLength": 1, + "description": "The name of the resume." + }, + "slug": { + "type": "string", + "minLength": 1, + "description": "The slug of the resume." + }, "tags": { "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "description": "The tags of the resume." }, - "isPublic": { "type": "boolean", "description": "Whether the resume is public." }, - "isLocked": { "type": "boolean", "description": "Whether the resume is locked." }, - "data": { "$ref": "#/components/schemas/ResumeData" }, - "hasPassword": { "type": "boolean" } + "isPublic": { + "type": "boolean", + "description": "Whether the resume is public." + }, + "isLocked": { + "type": "boolean", + "description": "Whether the resume is locked." + }, + "data": { + "$ref": "#/components/schemas/ResumeData" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date", + "description": "The date and time the resume was last updated." + }, + "hasPassword": { + "type": "boolean" + } }, - "required": ["id", "name", "slug", "tags", "isPublic", "isLocked", "data", "hasPassword"] + "required": ["id", "name", "slug", "tags", "isPublic", "isLocked", "data", "updatedAt", "hasPassword"] } } } @@ -2808,9 +8352,15 @@ { "type": "object", "properties": { - "defined": { "const": true }, - "code": { "const": "INVALID_PATCH_OPERATIONS" }, - "status": { "const": 400 }, + "defined": { + "const": true + }, + "code": { + "const": "INVALID_PATCH_OPERATIONS" + }, + "status": { + "const": 400 + }, "message": { "type": "string", "default": "The patch operations are invalid or produced an invalid resume." @@ -2822,10 +8372,68 @@ { "type": "object", "properties": { - "defined": { "const": false }, - "code": { "type": "string" }, - "status": { "type": "number" }, - "message": { "type": "string" }, + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": ["defined", "code", "status", "message"] + } + ] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "RESUME_VERSION_CONFLICT" + }, + "status": { + "const": 409 + }, + "message": { + "type": "string", + "default": "The resume changed after this patch was generated." + }, + "data": {} + }, + "required": ["defined", "code", "status", "message"] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, "data": {} }, "required": ["defined", "code", "status", "message"] @@ -2847,13 +8455,29 @@ "name": "id", "in": "path", "required": true, - "schema": { "type": "string", "description": "The ID of the resume." } + "schema": { + "type": "string", + "description": "The ID of the resume." + } } ], "responses": { "200": { "description": "The resume and its associated files were deleted successfully.", - "content": { "application/json": { "schema": { "anyOf": [{ "not": {} }, { "not": {} }] } } } + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "not": {} + }, + { + "not": {} + } + ] + } + } + } } } } @@ -2865,8 +8489,22 @@ "description": "Returns a publicly shared resume identified by the owner's username and the resume's slug. If the resume is password-protected and the viewer has not yet verified the password, a 401 error with code NEED_PASSWORD is returned. No authentication required for public resumes; if authenticated as the owner, private resumes are also accessible.", "tags": ["Resume Sharing"], "parameters": [ - { "name": "username", "in": "path", "required": true, "schema": { "type": "string" } }, - { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } } + { + "name": "username", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } ], "responses": { "200": { @@ -2876,19 +8514,38 @@ "schema": { "type": "object", "properties": { - "id": { "type": "string", "description": "The ID of the resume." }, - "name": { "type": "string", "minLength": 1, "description": "The name of the resume." }, - "slug": { "type": "string", "minLength": 1, "description": "The slug of the resume." }, + "id": { + "type": "string", + "description": "The ID of the resume." + }, + "slug": { + "type": "string", + "minLength": 1, + "description": "The slug of the resume." + }, "tags": { "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "description": "The tags of the resume." }, - "isPublic": { "type": "boolean", "description": "Whether the resume is public." }, - "isLocked": { "type": "boolean", "description": "Whether the resume is locked." }, - "data": { "$ref": "#/components/schemas/ResumeData" } + "isPublic": { + "type": "boolean", + "description": "Whether the resume is public." + }, + "isLocked": { + "type": "boolean", + "description": "Whether the resume is locked." + }, + "data": { + "$ref": "#/components/schemas/ResumeData" + }, + "name": { + "type": "string" + } }, - "required": ["id", "name", "slug", "tags", "isPublic", "isLocked", "data"] + "required": ["id", "slug", "tags", "isPublic", "isLocked", "data", "name"] } } } @@ -2908,7 +8565,11 @@ "application/json": { "schema": { "type": "object", - "properties": { "data": { "$ref": "#/components/schemas/ResumeData" } }, + "properties": { + "data": { + "$ref": "#/components/schemas/ResumeData" + } + }, "required": ["data"] } } @@ -2918,7 +8579,12 @@ "200": { "description": "The ID of the imported resume.", "content": { - "application/json": { "schema": { "type": "string", "description": "The ID of the imported resume." } } + "application/json": { + "schema": { + "type": "string", + "description": "The ID of the imported resume." + } + } } }, "400": { @@ -2930,10 +8596,19 @@ { "type": "object", "properties": { - "defined": { "const": true }, - "code": { "const": "RESUME_SLUG_ALREADY_EXISTS" }, - "status": { "const": 400 }, - "message": { "type": "string", "default": "A resume with this slug already exists." }, + "defined": { + "const": true + }, + "code": { + "const": "RESUME_SLUG_ALREADY_EXISTS" + }, + "status": { + "const": 400 + }, + "message": { + "type": "string", + "default": "A resume with this slug already exists." + }, "data": {} }, "required": ["defined", "code", "status", "message"] @@ -2941,10 +8616,18 @@ { "type": "object", "properties": { - "defined": { "const": false }, - "code": { "type": "string" }, - "status": { "type": "number" }, - "message": { "type": "string" }, + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, "data": {} }, "required": ["defined", "code", "status", "message"] @@ -2968,7 +8651,10 @@ "name": "id", "in": "path", "required": true, - "schema": { "type": "string", "description": "The ID of the resume." } + "schema": { + "type": "string", + "description": "The ID of the resume." + } } ], "requestBody": { @@ -2977,7 +8663,12 @@ "application/json": { "schema": { "type": "object", - "properties": { "isLocked": { "type": "boolean", "description": "Whether the resume is locked." } }, + "properties": { + "isLocked": { + "type": "boolean", + "description": "Whether the resume is locked." + } + }, "required": ["isLocked"] } } @@ -2986,7 +8677,20 @@ "responses": { "200": { "description": "The resume lock status was updated successfully.", - "content": { "application/json": { "schema": { "anyOf": [{ "not": {} }, { "not": {} }] } } } + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "not": {} + }, + { + "not": {} + } + ] + } + } + } } } } @@ -3002,7 +8706,10 @@ "name": "id", "in": "path", "required": true, - "schema": { "type": "string", "description": "The ID of the resume." } + "schema": { + "type": "string", + "description": "The ID of the resume." + } } ], "requestBody": { @@ -3011,7 +8718,13 @@ "application/json": { "schema": { "type": "object", - "properties": { "password": { "type": "string", "minLength": 6, "maxLength": 64 } }, + "properties": { + "password": { + "type": "string", + "minLength": 6, + "maxLength": 64 + } + }, "required": ["password"] } } @@ -3020,7 +8733,20 @@ "responses": { "200": { "description": "The resume password was set successfully.", - "content": { "application/json": { "schema": { "anyOf": [{ "not": {} }, { "not": {} }] } } } + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "not": {} + }, + { + "not": {} + } + ] + } + } + } } } }, @@ -3034,13 +8760,29 @@ "name": "id", "in": "path", "required": true, - "schema": { "type": "string", "description": "The ID of the resume." } + "schema": { + "type": "string", + "description": "The ID of the resume." + } } ], "responses": { "200": { "description": "The resume password was removed successfully.", - "content": { "application/json": { "schema": { "anyOf": [{ "not": {} }, { "not": {} }] } } } + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "not": {} + }, + { + "not": {} + } + ] + } + } + } } } } @@ -3056,13 +8798,21 @@ "name": "username", "in": "path", "required": true, - "schema": { "type": "string", "minLength": 1, "description": "The username of the resume owner." } + "schema": { + "type": "string", + "minLength": 1, + "description": "The username of the resume owner." + } }, { "name": "slug", "in": "path", "required": true, - "schema": { "type": "string", "minLength": 1, "description": "The slug of the resume." } + "schema": { + "type": "string", + "minLength": 1, + "description": "The slug of the resume." + } } ], "requestBody": { @@ -3072,7 +8822,11 @@ "schema": { "type": "object", "properties": { - "password": { "type": "string", "minLength": 1, "description": "The password to verify." } + "password": { + "type": "string", + "minLength": 1, + "description": "The password to verify." + } }, "required": ["password"] } @@ -3082,7 +8836,13 @@ "responses": { "200": { "description": "The password was verified successfully and access has been granted.", - "content": { "application/json": { "schema": { "type": "boolean" } } } + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } } } } @@ -3098,7 +8858,10 @@ "name": "id", "in": "path", "required": true, - "schema": { "type": "string", "description": "The ID of the resume." } + "schema": { + "type": "string", + "description": "The ID of the resume." + } } ], "requestBody": { @@ -3108,9 +8871,23 @@ "schema": { "type": "object", "properties": { - "name": { "type": "string", "minLength": 1, "description": "The name of the resume." }, - "slug": { "type": "string", "minLength": 1, "description": "The slug of the resume." }, - "tags": { "type": "array", "items": { "type": "string" }, "description": "The tags of the resume." } + "name": { + "type": "string", + "minLength": 1, + "description": "The name of the resume." + }, + "slug": { + "type": "string", + "minLength": 1, + "description": "The slug of the resume." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The tags of the resume." + } }, "required": ["name", "slug", "tags"] } @@ -3121,7 +8898,147 @@ "200": { "description": "The ID of the duplicated resume.", "content": { - "application/json": { "schema": { "type": "string", "description": "The ID of the duplicated resume." } } + "application/json": { + "schema": { + "type": "string", + "description": "The ID of the duplicated resume." + } + } + } + } + } + } + }, + "/resumes/{resumeId}/versions": { + "get": { + "operationId": "listResumeVersions", + "summary": "List resume version history", + "description": "Returns the recent version-history snapshots for a resume (id, label, and timestamp only). Snapshots are taken at milestones such as imports, AI edits, and periodic manual saves. Only the resume owner can list versions. Requires authentication.", + "tags": ["Resumes"], + "parameters": [ + { + "name": "resumeId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The ID of the resume whose version history to list." + } + } + ], + "responses": { + "200": { + "description": "A list of recent version snapshots, newest first.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the version snapshot." + }, + "label": { + "type": "string", + "description": "A short description of what triggered the snapshot." + }, + "createdAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date", + "description": "The date and time the snapshot was taken." + } + }, + "required": ["id", "label", "createdAt"] + } + } + } + } + } + } + } + }, + "/resumes/{resumeId}/versions/{versionId}/restore": { + "post": { + "operationId": "restoreResumeVersion", + "summary": "Restore a resume version", + "description": "Non-destructively restores a resume to a previous version snapshot by writing that snapshot's data back through the normal update path. Prior versions are preserved and the restore itself becomes a new snapshot. Only the resume owner can restore versions. Requires authentication.", + "tags": ["Resumes"], + "parameters": [ + { + "name": "resumeId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The ID of the resume to restore." + } + }, + { + "name": "versionId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The ID of the version snapshot to restore." + } + } + ], + "responses": { + "200": { + "description": "The restored resume with its full data.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the resume." + }, + "name": { + "type": "string", + "minLength": 1, + "description": "The name of the resume." + }, + "slug": { + "type": "string", + "minLength": 1, + "description": "The slug of the resume." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The tags of the resume." + }, + "isPublic": { + "type": "boolean", + "description": "Whether the resume is public." + }, + "isLocked": { + "type": "boolean", + "description": "Whether the resume is locked." + }, + "data": { + "$ref": "#/components/schemas/ResumeData" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date", + "description": "The date and time the resume was last updated." + }, + "hasPassword": { + "type": "boolean" + } + }, + "required": ["id", "name", "slug", "tags", "isPublic", "isLocked", "data", "updatedAt", "hasPassword"] + } + } } } } @@ -3138,14 +9055,42 @@ "name": "id", "in": "path", "required": true, - "schema": { "type": "string", "description": "The ID of the resume." } + "schema": { + "type": "string", + "description": "The ID of the resume." + } + }, + { + "name": "target", + "in": "query", + "schema": { + "enum": ["resume", "cover-letter"], + "type": "string", + "description": "Which document to download." + }, + "allowEmptyValue": true, + "allowReserved": true } ], "responses": { "200": { "description": "The generated resume PDF.", - "headers": { "content-disposition": { "schema": { "type": "string" }, "required": true } }, - "content": { "application/pdf": { "schema": { "type": "string", "contentMediaType": "application/pdf" } } } + "headers": { + "content-disposition": { + "schema": { + "type": "string" + }, + "required": true + } + }, + "content": { + "application/pdf": { + "schema": { + "type": "string", + "contentMediaType": "application/pdf" + } + } + } } } } @@ -3161,7 +9106,10 @@ "description": "The total number of registered users.", "content": { "application/json": { - "schema": { "type": "number", "description": "The total number of registered users." } + "schema": { + "type": "number", + "description": "The total number of registered users." + } } } } @@ -3179,7 +9127,10 @@ "description": "The total number of resumes created.", "content": { "application/json": { - "schema": { "type": "number", "description": "The total number of resumes created." } + "schema": { + "type": "number", + "description": "The total number of resumes created." + } } } } @@ -3196,7 +9147,12 @@ "200": { "description": "The number of GitHub stars for the Reactive Resume repository.", "content": { - "application/json": { "schema": { "type": "number", "description": "The number of GitHub stars." } } + "application/json": { + "schema": { + "type": "number", + "description": "The number of GitHub stars." + } + } } } } diff --git a/docs/superpowers/plans/2026-07-07-applications-rest-mcp-parity.md b/docs/superpowers/plans/2026-07-07-applications-rest-mcp-parity.md new file mode 100644 index 000000000..318614494 --- /dev/null +++ b/docs/superpowers/plans/2026-07-07-applications-rest-mcp-parity.md @@ -0,0 +1,1372 @@ +# Application Tracker REST and MCP Parity Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Expose every shipped Application Tracker capability through public REST/OpenAPI, Reactive Resume MCP tools, and the bundled `resume-builder` skill. + +**Architecture:** Reuse the existing `packages/api/src/features/applications` oRPC procedures and `applicationService` as the single business layer. Add the one missing public REST capability, application document attach/remove, then register MCP tools that call the injected in-process oRPC `RouterClient` rather than importing database code. + +**Tech Stack:** TypeScript, oRPC, Zod v4, Drizzle, Vitest, MCP TypeScript SDK, existing Reactive Resume storage service, Biome. + +## Global Constraints + +- Campaign tracking is out of scope. +- Fixed stages remain `saved`, `applied`, `screening`, `interview`, `offer`, `rejected`. +- Public REST endpoints live under `/api/openapi` and are generated from oRPC route metadata. +- Generic storage routes stay tagged `Internal`; public application document upload/remove belongs under the Applications tag. +- Application documents are PDFs only. +- Document uploads keep the existing 10MB storage limit. +- MCP tool names use canonical unprefixed `snake_case`. +- MCP handlers use the injected in-process oRPC `RouterClient`. +- No new dependencies. +- Do not refactor the Application Tracker UI. + +--- + +## File Structure + +- Modify `packages/api/src/dto/application.ts`: add document kind/upload/delete DTO schemas. +- Modify `packages/api/src/dto/application.test.ts`: test document DTO constraints. +- Modify `packages/api/src/features/applications/service.ts`: add `attachDocument` and `removeDocument` service methods, reusing existing ownership and storage cleanup helpers. +- Modify `packages/api/src/features/applications/service.test.ts`: cover document attachment/removal behavior and storage cleanup. +- Modify `packages/api/src/features/applications/crud.ts`: add public REST document routes. +- Modify `packages/api/src/features/applications/router.ts`: expose document route procedures under `applications`. +- Modify `packages/mcp/src/mcp-tool-names.ts`: add application MCP tool names. +- Modify `packages/mcp/src/tool-meta.ts`: add application tool input schemas and metadata. +- Modify `packages/mcp/src/tool-annotations.ts`: add annotations for new tools. +- Modify `packages/mcp/src/tools.ts`: register application tool handlers. +- Modify `packages/mcp/src/tool-annotations.test.ts`, `packages/mcp/src/mcp-server-card.test.ts`, and `packages/mcp/src/tools.test.ts`: cover registration and handler behavior. +- Modify `skills/resume-builder/SKILL.md`: tell agents application MCP tools exist. +- Modify `docs/guides/using-the-mcp-server.mdx`: document application tools and examples. +- Modify `docs/use-cases/api-mcp-resume-automation.mdx`: expand use case from resume-only automation to resume plus application tracking. +- Modify `docs/spec.json`: refresh OpenAPI output after API route changes. + +--- + +### Task 1: Public REST Document Attach/Remove + +**Files:** +- Modify: `packages/api/src/dto/application.ts` +- Modify: `packages/api/src/dto/application.test.ts` +- Modify: `packages/api/src/features/applications/service.ts` +- Modify: `packages/api/src/features/applications/service.test.ts` +- Modify: `packages/api/src/features/applications/crud.ts` +- Modify: `packages/api/src/features/applications/router.ts` + +**Interfaces:** +- Consumes: existing `applicationService.update(input)` and existing storage helpers in `packages/api/src/features/storage/service.ts`. +- Produces: + - `applicationDto.attachDocument.input`: `{ id: string; kind: "resume" | "cover-letter"; file: File }` + - `applicationDto.removeDocument.input`: `{ id: string; kind: "resume" | "cover-letter" }` + - `applicationService.attachDocument(input: { id: string; userId: string; kind: ApplicationDocumentKind; fileName: string; data: Uint8Array; contentType: string }): Promise` + - `applicationService.removeDocument(input: { id: string; userId: string; kind: ApplicationDocumentKind }): Promise` + - `crudRouter.attachDocument` + - `crudRouter.removeDocument` + +- [ ] **Step 1: Add failing DTO tests** + +Append to `packages/api/src/dto/application.test.ts`: + +```ts +describe("applicationDto document uploads", () => { + it("accepts PDF application documents", () => { + const file = new File(["%PDF-1.4"], "resume.pdf", { type: "application/pdf" }); + + const parsed = applicationDto.attachDocument.input.parse({ + id: "application-1", + kind: "resume", + file, + }); + + expect(parsed.kind).toBe("resume"); + expect(parsed.file.name).toBe("resume.pdf"); + }); + + it("rejects non-PDF application documents", () => { + const file = new File(["hello"], "cover.txt", { type: "text/plain" }); + + expect(() => + applicationDto.attachDocument.input.parse({ + id: "application-1", + kind: "cover-letter", + file, + }), + ).toThrow(); + }); + + it("rejects unknown application document kinds", () => { + const file = new File(["%PDF-1.4"], "resume.pdf", { type: "application/pdf" }); + + expect(() => + applicationDto.attachDocument.input.parse({ + id: "application-1", + kind: "portfolio", + file, + }), + ).toThrow(); + }); +}); +``` + +- [ ] **Step 2: Run DTO test to verify it fails** + +Run: + +```bash +pnpm --filter @reactive-resume/api test -- src/dto/application.test.ts +``` + +Expected: FAIL because `applicationDto.attachDocument` is undefined. + +- [ ] **Step 3: Add document DTO schemas** + +In `packages/api/src/dto/application.ts`, add near the top-level constants: + +```ts +const MAX_APPLICATION_DOCUMENT_BYTES = 10 * 1024 * 1024; + +const applicationDocumentKindSchema = z.enum(["resume", "cover-letter"]); + +const applicationDocumentFileSchema = z + .file() + .max(MAX_APPLICATION_DOCUMENT_BYTES, "File size must be less than 10MB") + .mime(["application/pdf"], "Application documents must be PDF files."); +``` + +Add to `applicationDto`: + +```ts + attachDocument: { + input: z.object({ + id: z.string(), + kind: applicationDocumentKindSchema, + file: applicationDocumentFileSchema, + }), + output: applicationSchema.omit({ userId: true }), + }, + + removeDocument: { + input: z.object({ + id: z.string(), + kind: applicationDocumentKindSchema, + }), + output: applicationSchema.omit({ userId: true }), + }, +``` + +Also export the kind type at the end of the file: + +```ts +export type ApplicationDocumentKind = z.infer; +``` + +- [ ] **Step 4: Run DTO tests to verify they pass** + +Run: + +```bash +pnpm --filter @reactive-resume/api test -- src/dto/application.test.ts +``` + +Expected: PASS. + +- [ ] **Step 5: Add failing service tests** + +Modify the hoisted mocks in `packages/api/src/features/applications/service.test.ts`: + +```ts +const storageDeleteMock = vi.hoisted(() => vi.fn()); +const uploadFileMock = vi.hoisted(() => vi.fn()); +``` + +Replace the storage service mock with: + +```ts +vi.mock("../storage/service", () => ({ + getStorageService: () => ({ delete: storageDeleteMock }), + uploadFile: uploadFileMock, +})); +``` + +Add to `beforeEach`: + +```ts + uploadFileMock.mockReset(); + uploadFileMock.mockResolvedValue({ url: "/api/uploads/user-1/pictures/new.pdf", key: "uploads/user-1/pictures/new.pdf" }); +``` + +Append tests: + +```ts +describe("applicationService.attachDocument", () => { + it("uploads a PDF resume document and stores it on the application", async () => { + const set = vi.fn(() => ({ where: () => ({ returning: () => Promise.resolve([{ ...existing }]) }) })); + dbMock.update.mockReturnValue({ set }); + + await applicationService.attachDocument({ + id: "app-1", + userId: "user-1", + kind: "resume", + fileName: "sent-resume.pdf", + contentType: "application/pdf", + data: new Uint8Array([1, 2, 3]), + }); + + expect(uploadFileMock).toHaveBeenCalledWith({ + userId: "user-1", + contentType: "application/pdf", + data: new Uint8Array([1, 2, 3]), + }); + expect(set).toHaveBeenCalledWith( + expect.objectContaining({ + resumeFileUrl: "/api/uploads/user-1/pictures/new.pdf", + resumeFileName: "sent-resume.pdf", + }), + ); + }); + + it("rejects non-PDF documents before upload", async () => { + await expect( + applicationService.attachDocument({ + id: "app-1", + userId: "user-1", + kind: "cover-letter", + fileName: "cover.txt", + contentType: "text/plain", + data: new Uint8Array([1]), + }), + ).rejects.toMatchObject({ code: "BAD_REQUEST" }); + + expect(uploadFileMock).not.toHaveBeenCalled(); + }); +}); + +describe("applicationService.removeDocument", () => { + it("clears and deletes an owned cover letter document", async () => { + const set = vi.fn(() => ({ where: () => ({ returning: () => Promise.resolve([{ ...existing }]) }) })); + dbMock.update.mockReturnValue({ set }); + + await applicationService.removeDocument({ id: "app-1", userId: "user-1", kind: "cover-letter" }); + + expect(set).toHaveBeenCalledWith( + expect.objectContaining({ + coverLetterUrl: null, + coverLetterName: null, + }), + ); + expect(storageDeleteMock).toHaveBeenCalledWith("uploads/user-1/pictures/cover.pdf"); + }); +}); +``` + +- [ ] **Step 6: Run service tests to verify they fail** + +Run: + +```bash +pnpm --filter @reactive-resume/api test -- src/features/applications/service.test.ts +``` + +Expected: FAIL because `applicationService.attachDocument` and `applicationService.removeDocument` are undefined. + +- [ ] **Step 7: Add service implementation** + +In `packages/api/src/features/applications/service.ts`, update imports: + +```ts +import type { ApplicationDocumentKind } from "../../dto/application"; +import { getStorageService, uploadFile } from "../storage/service"; +``` + +Add this helper near `deleteApplicationAttachments`: + +```ts +function documentFields(kind: ApplicationDocumentKind) { + return kind === "resume" + ? ({ + url: "resumeFileUrl", + name: "resumeFileName", + } as const) + : ({ + url: "coverLetterUrl", + name: "coverLetterName", + } as const); +} +``` + +Add methods to `applicationService` after `update`: + +```ts + attachDocument: async (input: { + id: string; + userId: string; + kind: ApplicationDocumentKind; + fileName: string; + data: Uint8Array; + contentType: string; + }) => { + if (input.contentType !== "application/pdf") { + throw new ORPCError("BAD_REQUEST", { message: "Application documents must be PDF files." }); + } + + const existing = await requireOwned(input.id, input.userId); + const fields = documentFields(input.kind); + const uploaded = await uploadFile({ + userId: input.userId, + data: input.data, + contentType: input.contentType, + }); + + try { + const updated = await applicationService.update({ + id: input.id, + userId: input.userId, + [fields.url]: uploaded.url, + [fields.name]: input.fileName, + }); + + await deleteApplicationAttachments(input.userId, [ + { + resumeFileUrl: fields.url === "resumeFileUrl" ? existing.resumeFileUrl : null, + coverLetterUrl: fields.url === "coverLetterUrl" ? existing.coverLetterUrl : null, + }, + ]); + + return updated; + } catch (error) { + await getStorageService().delete(uploaded.key).catch(() => false); + throw error; + } + }, + + removeDocument: async (input: { id: string; userId: string; kind: ApplicationDocumentKind }) => { + const existing = await requireOwned(input.id, input.userId); + const fields = documentFields(input.kind); + const updated = await applicationService.update({ + id: input.id, + userId: input.userId, + [fields.url]: null, + [fields.name]: null, + }); + + await deleteApplicationAttachments(input.userId, [ + { + resumeFileUrl: fields.url === "resumeFileUrl" ? existing.resumeFileUrl : null, + coverLetterUrl: fields.url === "coverLetterUrl" ? existing.coverLetterUrl : null, + }, + ]); + + return updated; + }, +``` + +- [ ] **Step 8: Run service tests to verify they pass** + +Run: + +```bash +pnpm --filter @reactive-resume/api test -- src/features/applications/service.test.ts +``` + +Expected: PASS. + +- [ ] **Step 9: Add REST routes** + +In `packages/api/src/features/applications/crud.ts`, add methods before `stats`: + +```ts + attachDocument: protectedProcedure + .route({ + method: "POST", + path: "/applications/{id}/documents/{kind}", + tags: ["Applications"], + operationId: "attachApplicationDocument", + summary: "Attach an application document", + description: + "Uploads and attaches a PDF document to an application. Kind must be either resume or cover-letter. Requires authentication.", + successDescription: "The updated application.", + requestBodyHint: "form-data", + }) + .input(applicationDto.attachDocument.input) + .use(resumeMutationRateLimit) + .output(applicationDto.attachDocument.output) + .handler(async ({ input, context }) => { + const buffer = await input.file.arrayBuffer(); + return applicationService.attachDocument({ + id: input.id, + userId: context.user.id, + kind: input.kind, + fileName: input.file.name, + contentType: input.file.type, + data: new Uint8Array(buffer), + }); + }), + + removeDocument: protectedProcedure + .route({ + method: "DELETE", + path: "/applications/{id}/documents/{kind}", + tags: ["Applications"], + operationId: "removeApplicationDocument", + summary: "Remove an application document", + description: + "Removes a resume or cover-letter PDF from an application and clears the stored document fields. Requires authentication.", + successDescription: "The updated application.", + }) + .input(applicationDto.removeDocument.input) + .use(resumeMutationRateLimit) + .output(applicationDto.removeDocument.output) + .handler(async ({ input, context }) => { + return applicationService.removeDocument({ id: input.id, userId: context.user.id, kind: input.kind }); + }), +``` + +In `packages/api/src/features/applications/router.ts`, add: + +```ts + attachDocument: crudRouter.attachDocument, + removeDocument: crudRouter.removeDocument, +``` + +- [ ] **Step 10: Run API typecheck** + +Run: + +```bash +pnpm --filter @reactive-resume/api typecheck +``` + +Expected: PASS. + +- [ ] **Step 11: Commit REST/API task** + +Run: + +```bash +git add packages/api/src/dto/application.ts packages/api/src/dto/application.test.ts packages/api/src/features/applications/service.ts packages/api/src/features/applications/service.test.ts packages/api/src/features/applications/crud.ts packages/api/src/features/applications/router.ts +git commit -m "feat: add application document API" +``` + +Expected: commit succeeds. + +--- + +### Task 2: MCP Tool Names, Metadata, and Annotations + +**Files:** +- Modify: `packages/mcp/src/mcp-tool-names.ts` +- Modify: `packages/mcp/src/tool-meta.ts` +- Modify: `packages/mcp/src/tool-annotations.ts` +- Modify: `packages/mcp/src/tool-annotations.test.ts` +- Modify: `packages/mcp/src/mcp-server-card.test.ts` + +**Interfaces:** +- Consumes: `applicationStatusSchema` and `contactSchema` from `@reactive-resume/schema/applications/data`. +- Produces: new `MCP_TOOL_NAME` values and `TOOL_META` entries used by `registerTools` in Task 3. + +- [ ] **Step 1: Add failing MCP metadata tests** + +In `packages/mcp/src/tool-annotations.test.ts`, add after the canonical read/patch names test: + +```ts + it("uses canonical application tool names", () => { + expect(MCP_TOOL_NAME.listApplications).toBe("list_applications"); + expect(MCP_TOOL_NAME.readApplication).toBe("read_application"); + expect(MCP_TOOL_NAME.createApplication).toBe("create_application"); + expect(MCP_TOOL_NAME.attachApplicationDocument).toBe("attach_application_document"); + expect(MCP_TOOL_NAME.autofillApplicationFromJob).toBe("autofill_application_from_job"); + }); +``` + +In the read-only annotation test list, add: + +```ts + MCP_TOOL_NAME.listApplications, + MCP_TOOL_NAME.readApplication, + MCP_TOOL_NAME.listApplicationTags, + MCP_TOOL_NAME.getApplicationStats, +``` + +Add tests before the open-world test: + +```ts + it("marks application delete tools as destructive", () => { + for (const name of [MCP_TOOL_NAME.deleteApplication, MCP_TOOL_NAME.bulkDeleteApplications]) { + const annotations = TOOL_ANNOTATIONS[name]; + expect(annotations.readOnlyHint, name).toBe(false); + expect(annotations.destructiveHint, name).toBe(true); + } + }); + + it("marks only job-posting autofill as open-world", () => { + expect(TOOL_ANNOTATIONS[MCP_TOOL_NAME.autofillApplicationFromJob].openWorldHint).toBe(true); + }); +``` + +In `packages/mcp/src/mcp-server-card.test.ts`, add: + +```ts + it("advertises application tracker tools", () => { + const names = card.tools.map((tool) => tool.name); + + expect(names).toContain("list_applications"); + expect(names).toContain("create_application"); + expect(names).toContain("attach_application_document"); + expect(names).toContain("tailor_resume_for_application"); + }); +``` + +- [ ] **Step 2: Run MCP metadata tests to verify they fail** + +Run: + +```bash +pnpm --filter @reactive-resume/mcp test -- src/tool-annotations.test.ts src/mcp-server-card.test.ts +``` + +Expected: FAIL because the new MCP tool names do not exist. + +- [ ] **Step 3: Add MCP tool names** + +In `packages/mcp/src/mcp-tool-names.ts`, add these properties to `MCP_TOOL_NAME`: + +```ts + listApplications: "list_applications", + readApplication: "read_application", + listApplicationTags: "list_application_tags", + getApplicationStats: "get_application_stats", + createApplication: "create_application", + updateApplication: "update_application", + addApplicationNote: "add_application_note", + deleteApplication: "delete_application", + bulkUpdateApplications: "bulk_update_applications", + bulkDeleteApplications: "bulk_delete_applications", + importApplications: "import_applications", + attachApplicationDocument: "attach_application_document", + removeApplicationDocument: "remove_application_document", + autofillApplicationFromJob: "autofill_application_from_job", + scoreApplicationMatch: "score_application_match", + tailorResumeForApplication: "tailor_resume_for_application", + draftApplicationMessage: "draft_application_message", +``` + +- [ ] **Step 4: Add MCP metadata schemas** + +In `packages/mcp/src/tool-meta.ts`, add imports: + +```ts +import { applicationStatusSchema, contactSchema } from "@reactive-resume/schema/applications/data"; +``` + +Add shared schemas near `resumeIdSchema`: + +```ts +const applicationIdSchema = z.string().min(1).describe(`Application ID. Use \`${T.listApplications}\` to find valid IDs.`); +const applicationDocumentKindSchema = z.enum(["resume", "cover-letter"]); +const pdfBase64Schema = z + .string() + .min(1) + .describe("Base64-encoded PDF bytes. Only application/pdf documents are accepted."); + +const applicationEditableSchema = { + company: z.string().min(1).optional().describe("Company name."), + role: z.string().min(1).optional().describe("Role or job title."), + status: applicationStatusSchema.optional().describe("Pipeline stage."), + archived: z.boolean().optional().describe("Whether the application is hidden from active views."), + location: z.string().nullable().optional(), + salary: z.string().nullable().optional(), + source: z.string().nullable().optional(), + sourceUrl: z.string().url().nullable().optional(), + jobDescription: z.string().max(20_000).nullable().optional(), + notes: z.string().nullable().optional(), + resumeId: z.string().nullable().optional(), + resumeFileUrl: z.string().nullable().optional(), + resumeFileName: z.string().nullable().optional(), + coverLetterUrl: z.string().nullable().optional(), + coverLetterName: z.string().nullable().optional(), + followUpAt: z.coerce.date().nullable().optional(), + followUpNote: z.string().nullable().optional(), + contacts: z.array(contactSchema).optional(), + tags: z.array(z.string()).optional(), +} as const; + +const createApplicationSchema = z.object({ + ...applicationEditableSchema, + company: z.string().min(1).describe("Company name."), + role: z.string().min(1).describe("Role or job title."), +}); +``` + +Add these `TOOL_META` entries after resume tools or in a separate Applications block: + +```ts + [T.listApplications]: { + title: "List Applications", + description: + "List job applications for the authenticated account. Use this before reading or updating existing applications.", + inputSchema: z.object({ + status: applicationStatusSchema.optional(), + tags: z.array(z.string()).optional().default([]), + includeArchived: z.boolean().optional().default(false), + }), + annotations: TOOL_ANNOTATIONS[T.listApplications], + }, + [T.readApplication]: { + title: "Read Application", + description: "Read one full job application, including contacts, document URLs, follow-up details, and timeline.", + inputSchema: z.object({ id: applicationIdSchema }), + annotations: TOOL_ANNOTATIONS[T.readApplication], + }, + [T.listApplicationTags]: { + title: "List Application Tags", + description: "Return every distinct tag used across job applications.", + inputSchema: z.object({}), + annotations: TOOL_ANNOTATIONS[T.listApplicationTags], + }, + [T.getApplicationStats]: { + title: "Get Application Stats", + description: "Return aggregate application counts by pipeline stage and source for insights.", + inputSchema: z.object({}), + annotations: TOOL_ANNOTATIONS[T.getApplicationStats], + }, + [T.createApplication]: { + title: "Create Application", + description: "Create a tracked job application. Company and role are required.", + inputSchema: createApplicationSchema, + annotations: TOOL_ANNOTATIONS[T.createApplication], + }, + [T.updateApplication]: { + title: "Update Application", + description: "Update application fields, move stages, archive/unarchive, edit contacts, follow-up, tags, or linked resume.", + inputSchema: z.object({ id: applicationIdSchema, ...applicationEditableSchema }), + annotations: TOOL_ANNOTATIONS[T.updateApplication], + }, + [T.addApplicationNote]: { + title: "Add Application Note", + description: "Append a free-text note to an application's timeline.", + inputSchema: z.object({ id: applicationIdSchema, text: z.string().min(1) }), + annotations: TOOL_ANNOTATIONS[T.addApplicationNote], + }, + [T.deleteApplication]: { + title: "Delete Application", + description: "Permanently delete one job application and its owned uploaded documents.", + inputSchema: z.object({ id: applicationIdSchema }), + annotations: TOOL_ANNOTATIONS[T.deleteApplication], + }, + [T.bulkUpdateApplications]: { + title: "Bulk Update Applications", + description: "Move, archive/unarchive, or add tags to multiple applications.", + inputSchema: z.object({ + ids: z.array(z.string()).min(1), + status: applicationStatusSchema.optional(), + archived: z.boolean().optional(), + addTags: z.array(z.string()).optional(), + }), + annotations: TOOL_ANNOTATIONS[T.bulkUpdateApplications], + }, + [T.bulkDeleteApplications]: { + title: "Bulk Delete Applications", + description: "Permanently delete multiple applications.", + inputSchema: z.object({ ids: z.array(z.string()).min(1) }), + annotations: TOOL_ANNOTATIONS[T.bulkDeleteApplications], + }, + [T.importApplications]: { + title: "Import Applications", + description: "Bulk-create application rows parsed from CSV or another source. Maximum 500 items.", + inputSchema: z.object({ items: z.array(createApplicationSchema).min(1).max(500) }), + annotations: TOOL_ANNOTATIONS[T.importApplications], + }, + [T.attachApplicationDocument]: { + title: "Attach Application Document", + description: "Attach a sent resume or cover-letter PDF to an application using base64-encoded PDF bytes.", + inputSchema: z.object({ + id: applicationIdSchema, + kind: applicationDocumentKindSchema, + fileName: z.string().min(1), + contentType: z.literal("application/pdf"), + dataBase64: pdfBase64Schema, + }), + annotations: TOOL_ANNOTATIONS[T.attachApplicationDocument], + }, + [T.removeApplicationDocument]: { + title: "Remove Application Document", + description: "Remove a sent resume or cover-letter PDF from an application.", + inputSchema: z.object({ id: applicationIdSchema, kind: applicationDocumentKindSchema }), + annotations: TOOL_ANNOTATIONS[T.removeApplicationDocument], + }, + [T.autofillApplicationFromJob]: { + title: "Autofill Application From Job", + description: "Use AI to extract company, role, location, salary, and job description from a job URL or pasted posting.", + inputSchema: z.object({ + sourceUrl: z.string().url().optional(), + jobDescription: z.string().max(20_000).optional(), + }), + annotations: TOOL_ANNOTATIONS[T.autofillApplicationFromJob], + }, + [T.scoreApplicationMatch]: { + title: "Score Application Match", + description: "Score the linked resume against the application's job description and persist match metadata.", + inputSchema: z.object({ id: applicationIdSchema }), + annotations: TOOL_ANNOTATIONS[T.scoreApplicationMatch], + }, + [T.tailorResumeForApplication]: { + title: "Tailor Resume For Application", + description: "Create and link a tailored copy of the application's linked resume.", + inputSchema: z.object({ id: applicationIdSchema }), + annotations: TOOL_ANNOTATIONS[T.tailorResumeForApplication], + }, + [T.draftApplicationMessage]: { + title: "Draft Application Message", + description: "Draft either a cover letter or recruiter follow-up from application and resume context.", + inputSchema: z.object({ id: applicationIdSchema, kind: z.enum(["cover-letter", "follow-up"]) }), + annotations: TOOL_ANNOTATIONS[T.draftApplicationMessage], + }, +``` + +- [ ] **Step 5: Add annotations** + +In `packages/mcp/src/tool-annotations.ts`, add: + +```ts +const READ_OPEN_WORLD_NON_IDEMPOTENT: ToolAnnotations = { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, +}; +``` + +Add mappings: + +```ts + [MCP_TOOL_NAME.listApplications]: READ_IDEMPOTENT, + [MCP_TOOL_NAME.readApplication]: READ_IDEMPOTENT, + [MCP_TOOL_NAME.listApplicationTags]: READ_IDEMPOTENT, + [MCP_TOOL_NAME.getApplicationStats]: READ_IDEMPOTENT, + [MCP_TOOL_NAME.createApplication]: WRITE_NON_IDEMPOTENT, + [MCP_TOOL_NAME.updateApplication]: WRITE_IDEMPOTENT, + [MCP_TOOL_NAME.addApplicationNote]: WRITE_NON_IDEMPOTENT, + [MCP_TOOL_NAME.deleteApplication]: WRITE_DESTRUCTIVE, + [MCP_TOOL_NAME.bulkUpdateApplications]: WRITE_IDEMPOTENT, + [MCP_TOOL_NAME.bulkDeleteApplications]: WRITE_DESTRUCTIVE, + [MCP_TOOL_NAME.importApplications]: WRITE_NON_IDEMPOTENT, + [MCP_TOOL_NAME.attachApplicationDocument]: WRITE_NON_IDEMPOTENT, + [MCP_TOOL_NAME.removeApplicationDocument]: WRITE_IDEMPOTENT, + [MCP_TOOL_NAME.autofillApplicationFromJob]: READ_OPEN_WORLD_NON_IDEMPOTENT, + [MCP_TOOL_NAME.scoreApplicationMatch]: WRITE_NON_IDEMPOTENT, + [MCP_TOOL_NAME.tailorResumeForApplication]: WRITE_NON_IDEMPOTENT, + [MCP_TOOL_NAME.draftApplicationMessage]: READ_NON_IDEMPOTENT, +``` + +Update the existing "declares no tools as open-world by default" test to skip `MCP_TOOL_NAME.autofillApplicationFromJob`: + +```ts + for (const [name, annotations] of Object.entries(TOOL_ANNOTATIONS)) { + if (name === MCP_TOOL_NAME.autofillApplicationFromJob) continue; + expect(annotations.openWorldHint).toBe(false); + } +``` + +- [ ] **Step 6: Run MCP metadata tests** + +Run: + +```bash +pnpm --filter @reactive-resume/mcp test -- src/tool-annotations.test.ts src/mcp-server-card.test.ts +``` + +Expected: PASS. + +- [ ] **Step 7: Commit metadata task** + +Run: + +```bash +git add packages/mcp/src/mcp-tool-names.ts packages/mcp/src/tool-meta.ts packages/mcp/src/tool-annotations.ts packages/mcp/src/tool-annotations.test.ts packages/mcp/src/mcp-server-card.test.ts +git commit -m "feat: describe application MCP tools" +``` + +Expected: commit succeeds. + +--- + +### Task 3: MCP Application Tool Handlers + +**Files:** +- Modify: `packages/mcp/src/tools.ts` +- Modify: `packages/mcp/src/tools.test.ts` + +**Interfaces:** +- Consumes: `MCP_TOOL_NAME` values and `TOOL_META` entries from Task 2. +- Consumes: API procedures exposed from Task 1. +- Produces: registered MCP handlers for every application tool. + +- [ ] **Step 1: Add failing handler tests** + +In `packages/mcp/src/tools.test.ts`, change `ToolHandler` to accept any object: + +```ts +type ToolHandler = (input: Record) => Promise<{ + content: Array<{ type: "text"; text: string }>; + isError?: boolean; +}>; +``` + +Extend `clientMock` with: + +```ts + applications: { + list: vi.fn(), + getById: vi.fn(), + tags: vi.fn(), + stats: vi.fn(), + create: vi.fn(), + update: vi.fn(), + addNote: vi.fn(), + delete: vi.fn(), + bulkUpdate: vi.fn(), + bulkDelete: vi.fn(), + import: vi.fn(), + attachDocument: vi.fn(), + removeDocument: vi.fn(), + ai: { + autofill: vi.fn(), + matchScore: vi.fn(), + tailorResume: vi.fn(), + draftMessage: vi.fn(), + }, + }, +``` + +Add tests: + +```ts + it("registers application tracker tools", () => { + const { server, registered } = makeFakeServer(); + registerTools(server as never, clientMock as never, new Headers()); + + const names = registered.map((item) => item.name); + expect(names).toContain("list_applications"); + expect(names).toContain("create_application"); + expect(names).toContain("attach_application_document"); + expect(names).toContain("draft_application_message"); + }); + + it("lists applications as JSON", async () => { + clientMock.applications.list.mockResolvedValueOnce([{ id: "app-1", company: "Acme", role: "Engineer" }]); + const { server, registered } = makeFakeServer(); + registerTools(server as never, clientMock as never, new Headers()); + + const tool = registered.find((item) => item.name === "list_applications")!; + const result = await tool.handler({ includeArchived: true, tags: ["remote"] }); + + expect(clientMock.applications.list).toHaveBeenCalledWith({ includeArchived: true, tags: ["remote"] }); + expect(JSON.parse(result.content[0]!.text)).toEqual([{ id: "app-1", company: "Acme", role: "Engineer" }]); + }); + + it("creates applications through the router client", async () => { + clientMock.applications.create.mockResolvedValueOnce("app-1"); + const { server, registered } = makeFakeServer(); + registerTools(server as never, clientMock as never, new Headers()); + + const tool = registered.find((item) => item.name === "create_application")!; + const result = await tool.handler({ company: "Acme", role: "Engineer", status: "saved" }); + + expect(clientMock.applications.create).toHaveBeenCalledWith({ + company: "Acme", + role: "Engineer", + status: "saved", + }); + expect(JSON.parse(result.content[0]!.text)).toEqual({ id: "app-1" }); + }); + + it("attaches a base64 PDF document through the router client", async () => { + clientMock.applications.attachDocument.mockResolvedValueOnce({ id: "app-1", resumeFileName: "resume.pdf" }); + const { server, registered } = makeFakeServer(); + registerTools(server as never, clientMock as never, new Headers()); + + const tool = registered.find((item) => item.name === "attach_application_document")!; + const result = await tool.handler({ + id: "app-1", + kind: "resume", + fileName: "resume.pdf", + contentType: "application/pdf", + dataBase64: Buffer.from("%PDF-1.4").toString("base64"), + }); + + const call = clientMock.applications.attachDocument.mock.calls[0]?.[0] as { file: File }; + expect(call.file).toBeInstanceOf(File); + expect(call.file.name).toBe("resume.pdf"); + expect(call.file.type).toBe("application/pdf"); + expect(JSON.parse(result.content[0]!.text)).toEqual({ id: "app-1", resumeFileName: "resume.pdf" }); + }); + + it("rejects non-PDF application document attachments before calling the client", async () => { + const { server, registered } = makeFakeServer(); + registerTools(server as never, clientMock as never, new Headers()); + + const tool = registered.find((item) => item.name === "attach_application_document")!; + const result = await tool.handler({ + id: "app-1", + kind: "resume", + fileName: "resume.txt", + contentType: "text/plain", + dataBase64: Buffer.from("hello").toString("base64"), + }); + + expect(result.isError).toBe(true); + expect(clientMock.applications.attachDocument).not.toHaveBeenCalled(); + }); +``` + +- [ ] **Step 2: Run handler tests to verify they fail** + +Run: + +```bash +pnpm --filter @reactive-resume/mcp test -- src/tools.test.ts +``` + +Expected: FAIL because application tools are not registered. + +- [ ] **Step 3: Add JSON and File helpers** + +In `packages/mcp/src/tools.ts`, add an import: + +```ts +import { Buffer } from "node:buffer"; +``` + +Add helper near `text()`: + +```ts +function json(value: unknown): CallToolResult { + return text(JSON.stringify(value, null, 2)); +} + +function fileFromBase64(input: { fileName: string; contentType: string; dataBase64: string }): File { + if (input.contentType !== "application/pdf") throw new Error("Application documents must be PDF files."); + + const bytes = Buffer.from(input.dataBase64, "base64"); + if (bytes.length === 0) throw new Error("Application document cannot be empty."); + + return new File([bytes], input.fileName, { type: input.contentType }); +} +``` + +- [ ] **Step 4: Register read/discovery handlers** + +In `registerTools`, after resume statistics or in a clearly labelled Applications block, add: + +```ts + server.registerTool( + T.listApplications, + TOOL_META[T.listApplications], + withErrorHandling("listing applications", async (params) => { + return json(await client.applications.list(params as { status?: never; tags?: string[]; includeArchived?: boolean })); + }), + ); + + server.registerTool( + T.readApplication, + TOOL_META[T.readApplication], + withErrorHandling("reading application", async ({ id }: { id: string }) => { + return json(await client.applications.getById({ id })); + }), + ); + + server.registerTool( + T.listApplicationTags, + TOOL_META[T.listApplicationTags], + withErrorHandling("listing application tags", async () => { + return json(await client.applications.tags()); + }), + ); + + server.registerTool( + T.getApplicationStats, + TOOL_META[T.getApplicationStats], + withErrorHandling("getting application stats", async () => { + return json(await client.applications.stats()); + }), + ); +``` + +- [ ] **Step 5: Register write handlers** + +Add: + +```ts + server.registerTool( + T.createApplication, + TOOL_META[T.createApplication], + withErrorHandling("creating application", async (params) => { + const id = await client.applications.create(params as never); + return json({ id }); + }), + ); + + server.registerTool( + T.updateApplication, + TOOL_META[T.updateApplication], + withErrorHandling("updating application", async (params) => { + return json(await client.applications.update(params as never)); + }), + ); + + server.registerTool( + T.addApplicationNote, + TOOL_META[T.addApplicationNote], + withErrorHandling("adding application note", async ({ id, text: noteText }: { id: string; text: string }) => { + return json(await client.applications.addNote({ id, text: noteText })); + }), + ); + + server.registerTool( + T.deleteApplication, + TOOL_META[T.deleteApplication], + withErrorHandling("deleting application", async ({ id }: { id: string }) => { + await client.applications.delete({ id }); + return text(`Deleted application (${id}).`); + }), + ); + + server.registerTool( + T.bulkUpdateApplications, + TOOL_META[T.bulkUpdateApplications], + withErrorHandling("bulk updating applications", async (params) => { + return json(await client.applications.bulkUpdate(params as never)); + }), + ); + + server.registerTool( + T.bulkDeleteApplications, + TOOL_META[T.bulkDeleteApplications], + withErrorHandling("bulk deleting applications", async ({ ids }: { ids: string[] }) => { + return json(await client.applications.bulkDelete({ ids })); + }), + ); + + server.registerTool( + T.importApplications, + TOOL_META[T.importApplications], + withErrorHandling("importing applications", async (params) => { + return json(await client.applications.import(params as never)); + }), + ); +``` + +- [ ] **Step 6: Register document handlers** + +Add: + +```ts + server.registerTool( + T.attachApplicationDocument, + TOOL_META[T.attachApplicationDocument], + withErrorHandling( + "attaching application document", + async ({ + id, + kind, + fileName, + contentType, + dataBase64, + }: { + id: string; + kind: "resume" | "cover-letter"; + fileName: string; + contentType: string; + dataBase64: string; + }) => { + const file = fileFromBase64({ fileName, contentType, dataBase64 }); + return json(await client.applications.attachDocument({ id, kind, file })); + }, + ), + ); + + server.registerTool( + T.removeApplicationDocument, + TOOL_META[T.removeApplicationDocument], + withErrorHandling( + "removing application document", + async ({ id, kind }: { id: string; kind: "resume" | "cover-letter" }) => { + return json(await client.applications.removeDocument({ id, kind })); + }, + ), + ); +``` + +- [ ] **Step 7: Register AI handlers** + +Add: + +```ts + server.registerTool( + T.autofillApplicationFromJob, + TOOL_META[T.autofillApplicationFromJob], + withErrorHandling("autofilling application from job", async (params) => { + return json(await client.applications.ai.autofill(params as { sourceUrl?: string; jobDescription?: string })); + }), + ); + + server.registerTool( + T.scoreApplicationMatch, + TOOL_META[T.scoreApplicationMatch], + withErrorHandling("scoring application match", async ({ id }: { id: string }) => { + return json(await client.applications.ai.matchScore({ id })); + }), + ); + + server.registerTool( + T.tailorResumeForApplication, + TOOL_META[T.tailorResumeForApplication], + withErrorHandling("tailoring resume for application", async ({ id }: { id: string }) => { + return json(await client.applications.ai.tailorResume({ id })); + }), + ); + + server.registerTool( + T.draftApplicationMessage, + TOOL_META[T.draftApplicationMessage], + withErrorHandling("drafting application message", async ({ id, kind }: { id: string; kind: "cover-letter" | "follow-up" }) => { + return json(await client.applications.ai.draftMessage({ id, kind })); + }), + ); +``` + +- [ ] **Step 8: Run MCP handler tests** + +Run: + +```bash +pnpm --filter @reactive-resume/mcp test -- src/tools.test.ts +``` + +Expected: PASS. + +- [ ] **Step 9: Run MCP typecheck** + +Run: + +```bash +pnpm --filter @reactive-resume/mcp typecheck +``` + +Expected: PASS. + +- [ ] **Step 10: Commit MCP handler task** + +Run: + +```bash +git add packages/mcp/src/tools.ts packages/mcp/src/tools.test.ts +git commit -m "feat: add application MCP tools" +``` + +Expected: commit succeeds. + +--- + +### Task 4: Skill, MCP Docs, and OpenAPI Spec + +**Files:** +- Modify: `skills/resume-builder/SKILL.md` +- Modify: `docs/guides/using-the-mcp-server.mdx` +- Modify: `docs/use-cases/api-mcp-resume-automation.mdx` +- Modify: `docs/spec.json` + +**Interfaces:** +- Consumes: MCP tool names from Task 2 and route changes from Task 1. +- Produces: user-facing docs and skill guidance that describe application tracking through MCP. + +- [ ] **Step 1: Update the resume-builder skill** + +In `skills/resume-builder/SKILL.md`, add after the Workflow section heading and before "Step 1": + +```md +### MCP Application Tracking + +Reactive Resume MCP can manage job applications as well as resumes. When the user wants to track applications through an agent, use the application tools instead of asking them to open the web UI. + +- Use `list_applications` before changing existing records. +- Use `create_application` for one new opportunity or `import_applications` for spreadsheet/CSV rows. +- Use `update_application` to move stages, archive/unarchive, edit contacts, set follow-ups, link a resume, or update job details. +- Use `add_application_note` to log timeline activity. +- Use `attach_application_document` and `remove_application_document` for sent resume or cover-letter PDFs. +- Use `score_application_match`, `tailor_resume_for_application`, and `draft_application_message` for Application Copilot workflows after a linked resume and job description exist. +- Review AI-generated cover letters and follow-ups before sending them. +``` + +- [ ] **Step 2: Update the MCP server guide tool table** + +In `docs/guides/using-the-mcp-server.mdx`, extend the Available Tools table with: + +```md +| `list_applications` | List tracked job applications. Supports stage, tag, and archived filters | +| `read_application` | Read one full application record with contacts, follow-up details, documents, and timeline | +| `list_application_tags` | List every distinct tag used across applications | +| `get_application_stats` | Get aggregate application counts by stage and source | +| `create_application` | Create a tracked job application | +| `update_application` | Update fields, move stage, archive/unarchive, edit contacts/follow-ups/tags, or link a resume | +| `add_application_note` | Append a note to an application's activity timeline | +| `delete_application` | Permanently delete one application and its owned uploaded documents | +| `bulk_update_applications` | Move, archive/unarchive, or add tags to multiple applications | +| `bulk_delete_applications` | Permanently delete multiple applications | +| `import_applications` | Bulk-create parsed application rows, up to 500 items | +| `attach_application_document` | Attach a sent resume or cover-letter PDF from base64-encoded PDF bytes | +| `remove_application_document` | Remove a sent resume or cover-letter PDF | +| `autofill_application_from_job` | Use AI to extract job details from a URL or pasted job description | +| `score_application_match` | Score the linked resume against the application job description | +| `tailor_resume_for_application` | Create and link a tailored resume copy for an application | +| `draft_application_message` | Draft a cover letter or recruiter follow-up from application and resume context | +``` + +- [ ] **Step 3: Add application MCP usage examples** + +In the Usage Examples section of `docs/guides/using-the-mcp-server.mdx`, add a subsection after Browsing: + +```md +### Tracking Applications + +- "Create an application for Senior Frontend Engineer at Acme, stage saved, source LinkedIn." +- "List my archived applications tagged remote." +- "Move my Acme application to interview and add a note that the technical screen is next Tuesday." +- "Attach this resume PDF to the Acme application." +- "Score the resume linked to this application against the job description." +- "Create a tailored resume copy for this application." +- "Draft a follow-up message for the recruiter." +``` + +Add a troubleshooting row: + +```md +| "Application documents must be PDF files" | `attach_application_document` only accepts `contentType: "application/pdf"` and base64-encoded PDF bytes | +``` + +- [ ] **Step 4: Update the API/MCP automation use case** + +Replace the first paragraph in `docs/use-cases/api-mcp-resume-automation.mdx` with: + +```md +Reactive Resume supports automation through authenticated API access and an MCP server that lets compatible tools work with resumes and job applications. Agents can list, read, create, import, duplicate, and patch resumes, then track applications, move opportunities through stages, add notes and follow-ups, attach sent documents, and run Application Copilot actions. +``` + +Add to "Common automation workflows": + +```md +- Track job applications end to end from an MCP client. +- Import existing application rows from a spreadsheet parser. +- Move applications through stages and log timeline notes. +- Attach the resume or cover-letter PDF sent for an application. +- Score a linked resume against a job description and create a tailored resume copy. +- Draft cover letters and recruiter follow-ups from saved application context. +``` + +- [ ] **Step 5: Refresh docs/spec.json** + +Start the dev server in one terminal: + +```bash +dotenvx run -f .env.local -- pnpm dev +``` + +In another terminal, after the server reports it is listening on port 3000, run: + +```bash +curl -fsS http://localhost:3000/api/openapi/spec.json -o docs/spec.json +``` + +Expected: `docs/spec.json` contains `/applications` paths: + +```bash +rg -n '"/applications|attachApplicationDocument|removeApplicationDocument' docs/spec.json +``` + +Expected: matches for the application paths and document operations. + +- [ ] **Step 6: Run docs/skill checks** + +Run: + +```bash +rg -n "list_applications|attach_application_document|tailor_resume_for_application" skills/resume-builder/SKILL.md docs/guides/using-the-mcp-server.mdx docs/use-cases/api-mcp-resume-automation.mdx +rg -n '"/applications|attachApplicationDocument|removeApplicationDocument' docs/spec.json +``` + +Expected: all commands print matching lines. + +- [ ] **Step 7: Commit docs task** + +Run: + +```bash +git add skills/resume-builder/SKILL.md docs/guides/using-the-mcp-server.mdx docs/use-cases/api-mcp-resume-automation.mdx docs/spec.json +git commit -m "docs: document application MCP automation" +``` + +Expected: commit succeeds. + +--- + +### Task 5: Final Validation and Boundary Check + +**Files:** +- No planned source edits. + +**Interfaces:** +- Consumes: all previous task outputs. +- Produces: verified implementation ready for review. + +- [ ] **Step 1: Run focused tests** + +Run: + +```bash +pnpm --filter @reactive-resume/api test -- src/dto/application.test.ts src/features/applications/service.test.ts +pnpm --filter @reactive-resume/mcp test -- src/tools.test.ts src/tool-annotations.test.ts src/mcp-server-card.test.ts +``` + +Expected: PASS. + +- [ ] **Step 2: Run focused typechecks** + +Run: + +```bash +pnpm --filter @reactive-resume/api typecheck +pnpm --filter @reactive-resume/mcp typecheck +``` + +Expected: PASS. + +- [ ] **Step 3: Run boundary check** + +Run: + +```bash +pnpm exec turbo boundaries +``` + +Expected: PASS. + +- [ ] **Step 4: Run non-mutating Biome check on touched files** + +Run: + +```bash +pnpm exec biome check packages/api/src/dto/application.ts packages/api/src/dto/application.test.ts packages/api/src/features/applications/service.ts packages/api/src/features/applications/service.test.ts packages/api/src/features/applications/crud.ts packages/api/src/features/applications/router.ts packages/mcp/src/mcp-tool-names.ts packages/mcp/src/tool-meta.ts packages/mcp/src/tool-annotations.ts packages/mcp/src/tools.ts packages/mcp/src/tool-annotations.test.ts packages/mcp/src/mcp-server-card.test.ts packages/mcp/src/tools.test.ts skills/resume-builder/SKILL.md docs/guides/using-the-mcp-server.mdx docs/use-cases/api-mcp-resume-automation.mdx docs/spec.json +``` + +Expected: PASS. If Biome reports formatting fixes, run the same command with `--write`, inspect the diff, and commit only formatting changes. + +- [ ] **Step 5: Inspect final diff** + +Run: + +```bash +git status --short +git log --oneline -5 +``` + +Expected: worktree is clean after the task commits, and recent commits are the REST/API, MCP metadata, MCP handler, and docs commits from this plan. diff --git a/docs/superpowers/specs/2026-07-07-applications-rest-mcp-parity-design.md b/docs/superpowers/specs/2026-07-07-applications-rest-mcp-parity-design.md new file mode 100644 index 000000000..901ed9bd9 --- /dev/null +++ b/docs/superpowers/specs/2026-07-07-applications-rest-mcp-parity-design.md @@ -0,0 +1,215 @@ +# Application Tracker REST and MCP Parity + +## Status + +Approved design for implementation planning. Campaign tracking is out of scope because the current app, database schema, changelog, guides, and tests do not ship a campaign field or campaign endpoint. + +## Goal + +Make every shipped Application Tracker capability available outside the web UI through: + +- the public REST/OpenAPI surface under `/api/openapi`; +- Reactive Resume MCP tools for agent-only workflows; +- the bundled `resume-builder` skill, so agents know the application tracker tools exist. + +The result should let a user track applications through an MCP client alone: create records, import existing rows, move stages, log notes, manage contacts and follow-ups, attach sent documents, run AI copilot actions, and inspect pipeline health. + +## Current Shipped Capabilities + +The current application tracker supports: + +- application records with company, role, location, salary, source, source URL, job description, notes, tags, status, archive state, linked resume, sent resume PDF, cover-letter PDF, contacts, follow-up date/note, applied date, and activity timeline; +- fixed stages: `saved`, `applied`, `screening`, `interview`, `offer`, `rejected`; +- create, edit, archive/unarchive, mark rejected, move stage, delete; +- board, table, search, sort, tag filter, archived toggle, and command-palette open/create shortcuts; +- bulk table actions: move stage, add tag, archive, delete; +- CSV import with header mapping and a 500-row cap; +- insights from aggregate stage/source counts plus client-derived weekly application velocity; +- timeline notes and automatic created/stage activity entries; +- contacts embedded on each application; +- Application Copilot actions: job-posting autofill, resume match score, tailored resume copy, cover-letter draft, follow-up draft; +- PDF upload/remove for resume file and cover letter through the generic storage route. + +## Existing API Coverage + +The app already routes most behavior through `packages/api/src/features/applications`: + +- `list`, `getById`, `create`, `import`, `update`, `addNote`, `delete`; +- `bulkUpdate`, `bulkDelete`, `stats`, `tags`; +- `ai.autofill`, `ai.matchScore`, `ai.draftMessage`, `ai.tailorResume`. + +These procedures are already authenticated and user-scoped through `protectedProcedure` and `applicationService`. The implementation should reuse them instead of adding a parallel controller or new business layer. + +## REST Design + +Keep the existing application procedures and improve parity with the public OpenAPI surface. + +### Public Application Endpoints + +Existing endpoints remain: + +- `GET /applications` +- `GET /applications/{id}` +- `POST /applications` +- `POST /applications/import` +- `PUT /applications/{id}` +- `POST /applications/{id}/notes` +- `DELETE /applications/{id}` +- `POST /applications/bulk-update` +- `POST /applications/bulk-delete` +- `GET /applications/stats` +- `GET /applications/tags` +- `POST /applications/ai/autofill` +- `POST /applications/{id}/ai/match-score` +- `POST /applications/{id}/ai/draft-message` +- `POST /applications/{id}/ai/tailor-resume` + +Add public document endpoints under the Applications tag: + +- `POST /applications/{id}/documents/{kind}` where `kind` is `resume` or `cover-letter`. + - Accept a PDF file via OpenAPI file upload. + - Store through the existing storage service. + - Update only the matching application fields: + - resume: `resumeFileUrl`, `resumeFileName`; + - cover letter: `coverLetterUrl`, `coverLetterName`. + - Return the updated application. +- `DELETE /applications/{id}/documents/{kind}`. + - Delete the owned storage object best-effort. + - Clear only the matching application fields. + - Return the updated application. + +This makes the document upload/remove UI available through public REST without exposing the generic storage route, which remains tagged `Internal` because it also serves profile-picture use cases. + +### REST Behavior Details + +- Use existing ownership checks: every application mutation must be scoped by `userId`. +- Reject non-PDF document uploads, matching the UI. +- Keep file size at the existing storage limit: 10MB. +- Preserve existing delete cleanup for uploaded documents when an application is deleted. +- Do not add server-side search/sort endpoints for board/table parity. The UI derives search and sort from `list`, and external clients can do the same. `list` already exposes status, tags, and archive filtering. +- Do not add campaign fields or endpoints. +- Regenerate or update `docs/spec.json` so the public API reference includes all application endpoints. + +## MCP Design + +Add application tools to `packages/mcp` using the existing metadata pattern: + +- add names to `MCP_TOOL_NAME`; +- add schemas/descriptions to `TOOL_META`; +- add behavior hints to `TOOL_ANNOTATIONS`; +- register handlers in `registerTools`; +- let `buildMcpServerCard` derive tool entries from `TOOL_META`; +- add focused tests so tool registration, annotations, and server card stay synchronized. + +All MCP handlers should call the injected in-process oRPC `RouterClient`. They should not import database code or reimplement ownership checks. + +### MCP Tools + +Read/discovery: + +- `list_applications`: list applications, with optional `status`, `tags`, and `includeArchived`. +- `read_application`: read one full application by ID. +- `list_application_tags`: list distinct tags. +- `get_application_stats`: return aggregate stage/source counts. + +Write: + +- `create_application`: create an application. +- `update_application`: update editable fields, including stage, archive state, contacts, follow-up, tags, linked resume, source URL, job description, notes, and document URLs. +- `add_application_note`: append a timeline note. +- `delete_application`: permanently delete one application. +- `bulk_update_applications`: move, archive/unarchive, or add tags to multiple applications. +- `bulk_delete_applications`: permanently delete multiple applications. +- `import_applications`: bulk-create parsed application rows, matching the REST `import` shape. +- `attach_application_document`: attach a sent resume or cover letter PDF. +- `remove_application_document`: remove a sent resume or cover letter PDF. + +AI: + +- `autofill_application_from_job`: extract company, role, location, salary, and job description from a job posting URL or pasted job description. +- `score_application_match`: score the linked resume against the job description and persist match metadata. +- `tailor_resume_for_application`: create and link a tailored resume copy. +- `draft_application_message`: draft either a cover letter or follow-up message. + +### MCP Attachment Input + +`attach_application_document` should support agent-only use. The tool should accept: + +- `id`; +- `kind`: `resume` or `cover-letter`; +- `fileName`; +- `contentType`: `application/pdf`; +- `dataBase64`: PDF bytes encoded as base64. + +The handler can decode the base64 and call the same shared application-document service used by the public REST file upload. This avoids forcing MCP clients to provide browser `File` objects while still giving agents the same document capability as the UI. + +### MCP Output Shape + +Prefer JSON text outputs for data-bearing tools. Human-readable success strings are fine for destructive/simple tools only when no structured data is useful. For create/update/read/list/stats/AI tools, return JSON so agents can chain calls reliably. + +### Tool Annotations + +- Read tools: read-only, idempotent, non-destructive. +- AI generation tools: + - `autofill_application_from_job` is read-only but non-idempotent if it fetches a URL or calls AI. + - `draft_application_message` is read-only but non-idempotent. + - `score_application_match` and `tailor_resume_for_application` are write, non-idempotent. +- Delete tools are destructive. +- `update_application`, `bulk_update_applications`, and document remove are write/idempotent when the same input repeats, except tag append remains effectively idempotent because the service de-duplicates tags. +- `create_application`, `import_applications`, note append, and document attach are write/non-idempotent. +- `openWorldHint` should be `false` for closed account data tools and `true` only for job-posting autofill because it can fetch an external URL. + +## Skill Update + +Update `skills/resume-builder/SKILL.md` in place. + +Add concise guidance that Reactive Resume MCP can manage applications as well as resumes. The skill should tell agents: + +- use application tools when the user wants to track job applications, import a spreadsheet, move an opportunity through stages, add notes/contacts/follow-ups, attach sent documents, or use Application Copilot; +- use `list_applications` before acting on existing records; +- use `create_application` or `import_applications` for new records; +- use `tailor_resume_for_application` after a linked resume and job description exist; +- review AI-generated cover letters and follow-ups before sending. + +Do not turn the skill into a full API reference. Keep it short and point agents at MCP tools by name. + +## Documentation Updates + +Update: + +- `docs/guides/using-the-mcp-server.mdx`: add Application Tracker tools, examples, and troubleshooting notes. +- `docs/use-cases/api-mcp-resume-automation.mdx`: expand scope from resume-only automation to resume plus application tracking. +- `docs/spec.json`: refresh the public OpenAPI spec so docs show application routes. + +The existing job application and CSV guides already describe the UI behavior accurately and should not need broad rewrites. + +## Testing Plan + +Focused tests: + +- API/service tests for attaching/removing resume and cover-letter documents, including ownership and storage cleanup. +- DTO/router tests for application document upload constraints if the route logic is not already covered through service tests. +- MCP tests that: + - every `MCP_TOOL_NAME` has metadata and annotations; + - server card includes the new tools; + - `registerTools` registers application tools; + - one read handler and one write handler call the expected oRPC client procedure; + - base64 document attachment rejects non-PDF input. + +Validation commands: + +- `pnpm --filter @reactive-resume/api test` +- `pnpm --filter @reactive-resume/mcp test` +- `pnpm --filter @reactive-resume/api typecheck` +- `pnpm --filter @reactive-resume/mcp typecheck` +- `pnpm exec turbo boundaries` +- a focused Biome check on touched files; use a non-mutating check first unless formatting edits are intentionally being applied. + +## Non-Goals + +- Custom stages. +- Campaign tracking. +- Server-side board/search/sort endpoints. +- A standalone application schema resource in MCP. +- New dependencies. +- Refactoring the application tracker UI. diff --git a/docs/use-cases/api-mcp-resume-automation.mdx b/docs/use-cases/api-mcp-resume-automation.mdx index 14e798466..e3ac09e1c 100644 --- a/docs/use-cases/api-mcp-resume-automation.mdx +++ b/docs/use-cases/api-mcp-resume-automation.mdx @@ -3,7 +3,7 @@ title: "API and MCP Resume Automation" description: "Automate resume workflows in Reactive Resume with API keys, the Patch API, the MCP server, AI agent tools, and the JSON resume schema." --- -Reactive Resume supports resume automation through authenticated API access and an MCP server that lets compatible tools list, read, create, import, duplicate, and patch resumes. +Reactive Resume supports automation through authenticated API access and an MCP server that lets compatible tools work with resumes and job applications. Agents can list, read, create, import, duplicate, and patch resumes, then track applications, move opportunities through stages, add notes and follow-ups, attach sent documents, and run Application Copilot actions. ## Automation options @@ -26,6 +26,12 @@ You can build workflows that: - Read resume data for review or transformation. - Patch targeted fields without replacing the whole resume. - Connect an MCP-compatible client to operate on resumes with authenticated tools. +- Track job applications end to end from an MCP client. +- Import existing application rows from a spreadsheet parser. +- Move applications through stages and log timeline notes. +- Attach the resume or cover-letter PDF sent for an application. +- Score a linked resume against a job description and create a tailored resume copy. +- Draft cover letters and recruiter follow-ups from saved application context. The Patch API is the safest starting point for targeted resume updates because it is designed around explicit changes to existing resume data. diff --git a/package.json b/package.json index 9fffe423e..1e624ccda 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "reactive-resume", "description": "Reactive Resume is a free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume.", - "version": "5.2.1", + "version": "5.2.2", "private": true, "license": "MIT", "author": { @@ -22,11 +22,15 @@ "scripts": { "build": "turbo run build", "doctor": "turbo run doctor", - "check": "biome check --write --unsafe .", + "check": "biome check --write --unsafe . && pnpm lint:docs:fix && pnpm lint:actions", "db:generate": "turbo run db:generate --filter=@reactive-resume/db", "db:migrate": "turbo run db:migrate --filter=@reactive-resume/db", "db:studio": "turbo run db:studio --filter=@reactive-resume/db", "lingui:extract": "turbo run lingui:extract --filter=web", + "lint": "pnpm lint:docs && pnpm lint:actions", + "lint:actions": "github-actionlint -shellcheck= -pyflakes=", + "lint:docs": "markdownlint-cli2", + "lint:docs:fix": "markdownlint-cli2 --fix", "dev": "turbo run dev", "dev:web": "turbo run dev --filter=web", "knip": "knip", @@ -53,9 +57,11 @@ "@types/node": "^26.1.0", "@types/pg": "^8.20.0", "@vitest/coverage-v8": "^4.1.9", + "github-actionlint": "1.7.12", "happy-dom": "^20.10.6", "knip": "^6.24.0", "lefthook": "^2.1.9", + "markdownlint-cli2": "0.23.0", "pg": "^8.22.0", "turbo": "^2.10.3", "typescript": "^6.0.3", diff --git a/packages/api/src/dto/application.test.ts b/packages/api/src/dto/application.test.ts index 0fca8c32a..b91108b73 100644 --- a/packages/api/src/dto/application.test.ts +++ b/packages/api/src/dto/application.test.ts @@ -34,3 +34,52 @@ describe("applicationDto jobDescription", () => { ).toThrow(); }); }); + +describe("applicationDto document uploads", () => { + it("accepts PDF application documents", () => { + const file = new File(["%PDF-1.4"], "resume.pdf", { type: "application/pdf" }); + + const parsed = applicationDto.attachDocument.input.parse({ + id: "application-1", + kind: "resume", + file, + }); + + expect(parsed.kind).toBe("resume"); + expect(parsed.file.name).toBe("resume.pdf"); + }); + + it("rejects non-PDF application documents", () => { + const file = new File(["hello"], "cover.txt", { type: "text/plain" }); + + expect(() => + applicationDto.attachDocument.input.parse({ + id: "application-1", + kind: "cover-letter", + file, + }), + ).toThrow(); + }); + + it("rejects unknown application document kinds", () => { + const file = new File(["%PDF-1.4"], "resume.pdf", { type: "application/pdf" }); + + expect(() => + applicationDto.attachDocument.input.parse({ + id: "application-1", + kind: "portfolio", + file, + }), + ).toThrow(); + }); +}); + +describe("applicationDto zero-argument inputs", () => { + it("normalizes stats input to an empty object", () => { + expect(applicationDto.stats.input.parse(undefined)).toEqual({}); + }); + + it("normalizes tags input to an empty object", () => { + expect(applicationDto.tags.input.parse(undefined)).toEqual({}); + }); +}); diff --git a/packages/api/src/dto/application.ts b/packages/api/src/dto/application.ts index 38ee81d01..0fc0753fb 100644 --- a/packages/api/src/dto/application.ts +++ b/packages/api/src/dto/application.ts @@ -9,6 +9,14 @@ import { } from "@reactive-resume/schema/applications/data"; const MAX_APPLICATION_JOB_DESCRIPTION_CHARS = 20_000; +const MAX_APPLICATION_DOCUMENT_BYTES = 10 * 1024 * 1024; + +const applicationDocumentKindSchema = z.enum(["resume", "cover-letter"]); + +const applicationDocumentFileSchema = z + .file() + .max(MAX_APPLICATION_DOCUMENT_BYTES, "File size must be less than 10MB") + .mime(["application/pdf"], "Application documents must be PDF files."); const httpUrlSchema = z .string() @@ -124,6 +132,23 @@ export const applicationDto = { output: applicationSchema.omit({ userId: true }), }, + attachDocument: { + input: z.object({ + id: z.string(), + kind: applicationDocumentKindSchema, + file: applicationDocumentFileSchema, + }), + output: applicationSchema.omit({ userId: true }), + }, + + removeDocument: { + input: z.object({ + id: z.string(), + kind: applicationDocumentKindSchema, + }), + output: applicationSchema.omit({ userId: true }), + }, + addNote: { input: z.object({ id: z.string(), text: z.string().trim().min(1) }), output: applicationSchema.omit({ userId: true }), @@ -153,7 +178,7 @@ export const applicationDto = { // Aggregates for the Insights view. Everything else (funnel, sankey, tiles) is derived // client-side from these raw counts via computeInsights(). stats: { - input: z.void(), + input: z.object({}).optional().default({}), output: z.object({ total: z.number(), byStage: z.array(z.object({ status: applicationStatusSchema, count: z.number() })), @@ -162,7 +187,9 @@ export const applicationDto = { }, tags: { - input: z.void(), + input: z.object({}).optional().default({}), output: z.array(z.string()), }, }; + +export type ApplicationDocumentKind = z.infer; diff --git a/packages/api/src/features/applications/crud.ts b/packages/api/src/features/applications/crud.ts index 08727a3a6..d6d475ff7 100644 --- a/packages/api/src/features/applications/crud.ts +++ b/packages/api/src/features/applications/crud.ts @@ -97,6 +97,66 @@ export const crudRouter = { return applicationService.update({ userId: context.user.id, ...input }); }), + attachDocument: protectedProcedure + .route({ + method: "POST", + path: "/applications/{id}/documents/{kind}", + tags: ["Applications"], + operationId: "attachApplicationDocument", + summary: "Attach an application document", + description: + "Uploads and attaches a PDF document to an application. Kind must be either resume or cover-letter. Requires authentication.", + successDescription: "The updated application.", + spec: (current) => { + const requestBody = current.requestBody; + if (!requestBody || "$ref" in requestBody) return current; + + const multipart = requestBody.content?.["multipart/form-data"]; + if (!multipart) return current; + + return { + ...current, + requestBody: { + ...requestBody, + content: { "multipart/form-data": multipart }, + }, + }; + }, + }) + .input(applicationDto.attachDocument.input) + .use(resumeMutationRateLimit) + .output(applicationDto.attachDocument.output) + .handler(async ({ input, context }) => { + const buffer = await input.file.arrayBuffer(); + + return applicationService.attachDocument({ + id: input.id, + userId: context.user.id, + kind: input.kind, + fileName: input.file.name, + contentType: input.file.type, + data: new Uint8Array(buffer), + }); + }), + + removeDocument: protectedProcedure + .route({ + method: "DELETE", + path: "/applications/{id}/documents/{kind}", + tags: ["Applications"], + operationId: "removeApplicationDocument", + summary: "Remove an application document", + description: + "Removes a resume or cover-letter PDF from an application and clears the stored document fields. Requires authentication.", + successDescription: "The updated application.", + }) + .input(applicationDto.removeDocument.input) + .use(resumeMutationRateLimit) + .output(applicationDto.removeDocument.output) + .handler(async ({ input, context }) => { + return applicationService.removeDocument({ id: input.id, userId: context.user.id, kind: input.kind }); + }), + addNote: protectedProcedure .route({ method: "POST", diff --git a/packages/api/src/features/applications/router.ts b/packages/api/src/features/applications/router.ts index 38b91707f..8807ef77a 100644 --- a/packages/api/src/features/applications/router.ts +++ b/packages/api/src/features/applications/router.ts @@ -7,6 +7,8 @@ export const applicationsRouter = { create: crudRouter.create, import: crudRouter.import, update: crudRouter.update, + attachDocument: crudRouter.attachDocument, + removeDocument: crudRouter.removeDocument, addNote: crudRouter.addNote, delete: crudRouter.delete, bulkUpdate: crudRouter.bulkUpdate, diff --git a/packages/api/src/features/applications/service.test.ts b/packages/api/src/features/applications/service.test.ts index 68e626d8b..f8224becc 100644 --- a/packages/api/src/features/applications/service.test.ts +++ b/packages/api/src/features/applications/service.test.ts @@ -9,6 +9,7 @@ const dbMock = vi.hoisted(() => ({ })); const resumeGetByIdMock = vi.hoisted(() => vi.fn()); const storageDeleteMock = vi.hoisted(() => vi.fn()); +const uploadFileMock = vi.hoisted(() => vi.fn()); vi.mock("@reactive-resume/db/client", () => ({ db: dbMock })); vi.mock("@reactive-resume/db/schema", () => ({ @@ -36,6 +37,7 @@ vi.mock("../resume/service", () => ({ })); vi.mock("../storage/service", () => ({ getStorageService: () => ({ delete: storageDeleteMock }), + uploadFile: uploadFileMock, })); const { applicationService } = await import("./service"); @@ -51,17 +53,34 @@ const existing = { coverLetterUrl: "/api/uploads/user-1/pictures/cover.pdf", }; -beforeEach(() => { +const createSelectChain = (rows: unknown[]) => ({ + from: () => ({ + where: () => Promise.resolve(rows), + }), +}); + +const setSelectResults = (...results: unknown[][]) => { dbMock.select.mockReset(); + for (const rows of results) { + dbMock.select.mockReturnValueOnce(createSelectChain(rows)); + } + dbMock.select.mockReturnValue(createSelectChain([])); +}; + +beforeEach(() => { dbMock.insert.mockReset(); dbMock.update.mockReset(); dbMock.delete.mockReset(); resumeGetByIdMock.mockReset(); storageDeleteMock.mockReset(); + uploadFileMock.mockReset(); resumeGetByIdMock.mockResolvedValue({ id: "resume-1" }); storageDeleteMock.mockResolvedValue(true); - // requireOwned: db.select().from().where() resolves to [existing] - dbMock.select.mockReturnValue({ from: () => ({ where: () => Promise.resolve([{ ...existing }]) }) }); + uploadFileMock.mockResolvedValue({ + url: "/api/uploads/user-1/pictures/new.pdf", + key: "uploads/user-1/pictures/new.pdf", + }); + setSelectResults([{ ...existing }]); }); describe("applicationService.create", () => { @@ -136,22 +155,129 @@ describe("applicationService.delete", () => { }); }); +describe("applicationService.attachDocument", () => { + it("uploads a PDF resume document and stores it on the application", async () => { + setSelectResults([{ ...existing }], [{ ...existing }], []); + const set = vi.fn(() => ({ where: () => ({ returning: () => Promise.resolve([{ ...existing }]) }) })); + dbMock.update.mockReturnValue({ set }); + + await applicationService.attachDocument({ + id: "app-1", + userId: "user-1", + kind: "resume", + fileName: "sent-resume.pdf", + contentType: "application/pdf", + data: new Uint8Array([1, 2, 3]), + }); + + expect(uploadFileMock).toHaveBeenCalledWith({ + userId: "user-1", + contentType: "application/pdf", + data: new Uint8Array([1, 2, 3]), + }); + expect(set).toHaveBeenCalledWith( + expect.objectContaining({ + resumeFileUrl: "/api/uploads/user-1/pictures/new.pdf", + resumeFileName: "sent-resume.pdf", + }), + ); + }); + + it("rejects non-PDF documents before upload", async () => { + await expect( + applicationService.attachDocument({ + id: "app-1", + userId: "user-1", + kind: "cover-letter", + fileName: "cover.txt", + contentType: "text/plain", + data: new Uint8Array([1]), + }), + ).rejects.toMatchObject({ code: "BAD_REQUEST" }); + + expect(uploadFileMock).not.toHaveBeenCalled(); + }); + + it("does not delete the replaced upload when another application still references it", async () => { + setSelectResults( + [{ ...existing }], + [{ ...existing }], + [ + { + id: "app-2", + resumeFileUrl: existing.resumeFileUrl, + coverLetterUrl: null, + }, + ], + ); + const set = vi.fn(() => ({ where: () => ({ returning: () => Promise.resolve([{ ...existing }]) }) })); + dbMock.update.mockReturnValue({ set }); + + await applicationService.attachDocument({ + id: "app-1", + userId: "user-1", + kind: "resume", + fileName: "sent-resume.pdf", + contentType: "application/pdf", + data: new Uint8Array([1, 2, 3]), + }); + + expect(storageDeleteMock).not.toHaveBeenCalledWith("uploads/user-1/pictures/resume.pdf"); + }); +}); + +describe("applicationService.removeDocument", () => { + it("clears and deletes an owned cover letter document", async () => { + setSelectResults([{ ...existing }], [{ ...existing }], []); + const set = vi.fn(() => ({ where: () => ({ returning: () => Promise.resolve([{ ...existing }]) }) })); + dbMock.update.mockReturnValue({ set }); + + await applicationService.removeDocument({ id: "app-1", userId: "user-1", kind: "cover-letter" }); + + expect(set).toHaveBeenCalledWith( + expect.objectContaining({ + coverLetterUrl: null, + coverLetterName: null, + }), + ); + expect(storageDeleteMock).toHaveBeenCalledWith("uploads/user-1/pictures/cover.pdf"); + }); + + it("does not delete a removed upload while another application still references it", async () => { + setSelectResults( + [{ ...existing }], + [{ ...existing }], + [ + { + id: "app-2", + resumeFileUrl: null, + coverLetterUrl: existing.coverLetterUrl, + }, + ], + ); + const set = vi.fn(() => ({ where: () => ({ returning: () => Promise.resolve([{ ...existing }]) }) })); + dbMock.update.mockReturnValue({ set }); + + await applicationService.removeDocument({ id: "app-1", userId: "user-1", kind: "cover-letter" }); + + expect(storageDeleteMock).not.toHaveBeenCalledWith("uploads/user-1/pictures/cover.pdf"); + }); +}); + describe("applicationService.bulkDelete", () => { it("deletes uploaded attachments for deleted owned applications", async () => { - dbMock.select.mockReturnValue({ - from: () => ({ - where: () => - Promise.resolve([ - { ...existing, id: "app-1" }, - { - ...existing, - id: "app-2", - resumeFileUrl: "http://localhost:3000/api/uploads/user-2/pictures/ignored.pdf", - coverLetterUrl: null, - }, - ]), - }), - }); + setSelectResults( + [ + { ...existing, id: "app-1" }, + { + ...existing, + id: "app-2", + resumeFileUrl: "http://localhost:3000/api/uploads/user-2/pictures/ignored.pdf", + coverLetterUrl: null, + }, + ], + [], + ); dbMock.delete.mockReturnValue({ where: () => ({ returning: () => Promise.resolve([{ id: "app-1" }, { id: "app-2" }]) }), }); diff --git a/packages/api/src/features/applications/service.ts b/packages/api/src/features/applications/service.ts index f68b62368..ea03b4037 100644 --- a/packages/api/src/features/applications/service.ts +++ b/packages/api/src/features/applications/service.ts @@ -1,4 +1,5 @@ import type { ActivityEvent, AiMetadata, ApplicationStatus, Contact } from "@reactive-resume/schema/applications/data"; +import type { ApplicationDocumentKind } from "../../dto/application"; import { ORPCError } from "@orpc/client"; import { and, arrayContains, desc, eq, inArray, sql } from "drizzle-orm"; import { db } from "@reactive-resume/db/client"; @@ -6,7 +7,7 @@ import * as schema from "@reactive-resume/db/schema"; import { STAGES } from "@reactive-resume/schema/applications/data"; import { generateId } from "@reactive-resume/utils/string"; import { resumeService } from "../resume/service"; -import { getStorageService } from "../storage/service"; +import { getStorageService, uploadFile } from "../storage/service"; const stageLabel = (status: ApplicationStatus) => STAGES.find((s) => s.value === status)?.label ?? status; @@ -79,7 +80,7 @@ async function deleteApplicationAttachments( userId: string, applications: { resumeFileUrl?: string | null; coverLetterUrl?: string | null }[], ) { - const keys = [ + const candidateKeys = [ ...new Set( applications.flatMap((application) => [ storageKeyFromApplicationUrl(userId, application.resumeFileUrl), @@ -88,11 +89,41 @@ async function deleteApplicationAttachments( ), ].filter((key): key is string => !!key); + if (candidateKeys.length === 0) return; + + const remainingApplications = await db + .select({ + resumeFileUrl: schema.application.resumeFileUrl, + coverLetterUrl: schema.application.coverLetterUrl, + }) + .from(schema.application) + .where(eq(schema.application.userId, userId)); + + const referencedKeys = new Set( + remainingApplications.flatMap((application) => [ + storageKeyFromApplicationUrl(userId, application.resumeFileUrl), + storageKeyFromApplicationUrl(userId, application.coverLetterUrl), + ]), + ); + const keys = candidateKeys.filter((key) => !referencedKeys.has(key)); + if (keys.length === 0) return; const storageService = getStorageService(); await Promise.allSettled(keys.map((key) => storageService.delete(key))); } +function documentFields(kind: ApplicationDocumentKind) { + return kind === "resume" + ? ({ + url: "resumeFileUrl", + name: "resumeFileName", + } as const) + : ({ + url: "coverLetterUrl", + name: "coverLetterName", + } as const); +} + const stripUserId = (row: T) => { const { userId: _userId, ...rest } = row; return rest; @@ -197,6 +228,70 @@ export const applicationService = { return stripUserId(updated); }, + attachDocument: async (input: { + id: string; + userId: string; + kind: ApplicationDocumentKind; + fileName: string; + data: Uint8Array; + contentType: string; + }) => { + if (input.contentType !== "application/pdf") { + throw new ORPCError("BAD_REQUEST", { message: "Application documents must be PDF files." }); + } + + const existing = await requireOwned(input.id, input.userId); + const fields = documentFields(input.kind); + const uploaded = await uploadFile({ + userId: input.userId, + data: input.data, + contentType: input.contentType, + }); + + try { + const updated = await applicationService.update({ + id: input.id, + userId: input.userId, + [fields.url]: uploaded.url, + [fields.name]: input.fileName, + }); + + await deleteApplicationAttachments(input.userId, [ + { + resumeFileUrl: fields.url === "resumeFileUrl" ? existing.resumeFileUrl : null, + coverLetterUrl: fields.url === "coverLetterUrl" ? existing.coverLetterUrl : null, + }, + ]); + + return updated; + } catch (error) { + await getStorageService() + .delete(uploaded.key) + .catch(() => false); + throw error; + } + }, + + removeDocument: async (input: { id: string; userId: string; kind: ApplicationDocumentKind }) => { + const existing = await requireOwned(input.id, input.userId); + const fields = documentFields(input.kind); + const updated = await applicationService.update({ + id: input.id, + userId: input.userId, + [fields.url]: null, + [fields.name]: null, + }); + + await deleteApplicationAttachments(input.userId, [ + { + resumeFileUrl: fields.url === "resumeFileUrl" ? existing.resumeFileUrl : null, + coverLetterUrl: fields.url === "coverLetterUrl" ? existing.coverLetterUrl : null, + }, + ]); + + return updated; + }, + // Persist AI-owned enrichment (match score + freeform metadata). Separate from the editable // update path so these fields are only ever written by the AI procedures. setAiResult: async (input: { diff --git a/packages/mcp/src/mcp-server-card.test.ts b/packages/mcp/src/mcp-server-card.test.ts index 5da96824a..e7f47a1f3 100644 --- a/packages/mcp/src/mcp-server-card.test.ts +++ b/packages/mcp/src/mcp-server-card.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { buildMcpServerCard } from "./mcp-server-card"; import { MCP_TOOL_NAME } from "./mcp-tool-names"; +import { TOOL_META } from "./tool-meta"; describe("buildMcpServerCard", () => { const card = buildMcpServerCard("1.2.3"); @@ -41,6 +42,15 @@ describe("buildMcpServerCard", () => { expect(tool?.annotations?.readOnlyHint).toBe(true); }); + it("advertises application tracker tools", () => { + const names = card.tools.map((tool) => tool.name); + + expect(names).toContain("list_applications"); + expect(names).toContain("create_application"); + expect(names).toContain("attach_application_document"); + expect(names).toContain("tailor_resume_for_application"); + }); + it("declares a JSON Schema input for every tool", () => { for (const tool of card.tools) { expect(tool.inputSchema, tool.name).toBeDefined(); @@ -71,4 +81,41 @@ describe("buildMcpServerCard", () => { const props = card.configurationSchema.properties as Record; expect(props.apiKey).toBeDefined(); }); + + it("matches the create/update application archived contract", () => { + const create = TOOL_META[MCP_TOOL_NAME.createApplication].inputSchema; + const update = TOOL_META[MCP_TOOL_NAME.updateApplication].inputSchema; + + expect(create.safeParse({ company: "Acme", role: "Engineer", archived: true }).success).toBe(false); + expect(update.safeParse({ id: "app-1", archived: true }).success).toBe(true); + }); + + it("accepts only http/https application source URLs", () => { + const create = TOOL_META[MCP_TOOL_NAME.createApplication].inputSchema; + const autofill = TOOL_META[MCP_TOOL_NAME.autofillApplicationFromJob].inputSchema; + + expect(create.safeParse({ company: "Acme", role: "Engineer", sourceUrl: "https://example.com/job" }).success).toBe( + true, + ); + expect(autofill.safeParse({ sourceUrl: "http://example.com/job" }).success).toBe(true); + expect(create.safeParse({ company: "Acme", role: "Engineer", sourceUrl: "ftp://example.com/job" }).success).toBe( + false, + ); + expect(autofill.safeParse({ sourceUrl: "javascript:alert(1)" }).success).toBe(false); + }); + + it("rejects application document payloads above 10MB decoded", () => { + const schema = TOOL_META[MCP_TOOL_NAME.attachApplicationDocument].inputSchema; + const tooLargePdf = Buffer.alloc(10 * 1024 * 1024 + 1, 0).toString("base64"); + + expect( + schema.safeParse({ + id: "app-1", + kind: "resume", + fileName: "resume.pdf", + contentType: "application/pdf", + dataBase64: tooLargePdf, + }).success, + ).toBe(false); + }); }); diff --git a/packages/mcp/src/mcp-tool-names.ts b/packages/mcp/src/mcp-tool-names.ts index 9d5cf01f1..7f00ae886 100644 --- a/packages/mcp/src/mcp-tool-names.ts +++ b/packages/mcp/src/mcp-tool-names.ts @@ -14,4 +14,21 @@ export const MCP_TOOL_NAME = { lockResume: "lock_resume", unlockResume: "unlock_resume", getResumeStatistics: "get_resume_statistics", + listApplications: "list_applications", + readApplication: "read_application", + listApplicationTags: "list_application_tags", + getApplicationStats: "get_application_stats", + createApplication: "create_application", + updateApplication: "update_application", + addApplicationNote: "add_application_note", + deleteApplication: "delete_application", + bulkUpdateApplications: "bulk_update_applications", + bulkDeleteApplications: "bulk_delete_applications", + importApplications: "import_applications", + attachApplicationDocument: "attach_application_document", + removeApplicationDocument: "remove_application_document", + autofillApplicationFromJob: "autofill_application_from_job", + scoreApplicationMatch: "score_application_match", + tailorResumeForApplication: "tailor_resume_for_application", + draftApplicationMessage: "draft_application_message", } as const; diff --git a/packages/mcp/src/tool-annotations.test.ts b/packages/mcp/src/tool-annotations.test.ts index 2130aa18d..deb89c299 100644 --- a/packages/mcp/src/tool-annotations.test.ts +++ b/packages/mcp/src/tool-annotations.test.ts @@ -15,6 +15,14 @@ describe("MCP_TOOL_NAME", () => { expect(MCP_TOOL_NAME.patchResume).toBe("apply_resume_patch"); }); + it("uses canonical application tool names", () => { + expect(MCP_TOOL_NAME.listApplications).toBe("list_applications"); + expect(MCP_TOOL_NAME.readApplication).toBe("read_application"); + expect(MCP_TOOL_NAME.createApplication).toBe("create_application"); + expect(MCP_TOOL_NAME.attachApplicationDocument).toBe("attach_application_document"); + expect(MCP_TOOL_NAME.autofillApplicationFromJob).toBe("autofill_application_from_job"); + }); + it("uses unique values for every tool", () => { const values = Object.values(MCP_TOOL_NAME); expect(new Set(values).size).toBe(values.length); @@ -35,6 +43,10 @@ describe("TOOL_ANNOTATIONS", () => { MCP_TOOL_NAME.getResume, MCP_TOOL_NAME.getResumeAnalysis, MCP_TOOL_NAME.getResumeStatistics, + MCP_TOOL_NAME.listApplications, + MCP_TOOL_NAME.readApplication, + MCP_TOOL_NAME.listApplicationTags, + MCP_TOOL_NAME.getApplicationStats, ]; for (const name of readOnlyTools) { const annotations = TOOL_ANNOTATIONS[name]; @@ -58,6 +70,14 @@ describe("TOOL_ANNOTATIONS", () => { expect(annotations.readOnlyHint).toBe(false); }); + it("marks application delete tools as destructive", () => { + for (const name of [MCP_TOOL_NAME.deleteApplication, MCP_TOOL_NAME.bulkDeleteApplications]) { + const annotations = TOOL_ANNOTATIONS[name]; + expect(annotations.readOnlyHint, name).toBe(false); + expect(annotations.destructiveHint, name).toBe(true); + } + }); + it("marks creation/import/duplicate as non-readonly and non-idempotent", () => { for (const name of [ MCP_TOOL_NAME.createResume, @@ -82,8 +102,13 @@ describe("TOOL_ANNOTATIONS", () => { } }); + it("marks only job-posting autofill as open-world", () => { + expect(TOOL_ANNOTATIONS[MCP_TOOL_NAME.autofillApplicationFromJob].openWorldHint).toBe(true); + }); + it("declares no tools as open-world by default", () => { - for (const annotations of Object.values(TOOL_ANNOTATIONS)) { + for (const [name, annotations] of Object.entries(TOOL_ANNOTATIONS)) { + if (name === MCP_TOOL_NAME.autofillApplicationFromJob) continue; expect(annotations.openWorldHint).toBe(false); } }); diff --git a/packages/mcp/src/tool-annotations.ts b/packages/mcp/src/tool-annotations.ts index 5b1edb865..5f3be0514 100644 --- a/packages/mcp/src/tool-annotations.ts +++ b/packages/mcp/src/tool-annotations.ts @@ -16,6 +16,12 @@ const READ_NON_IDEMPOTENT: ToolAnnotations = { idempotentHint: false, openWorldHint: false, }; +const READ_OPEN_WORLD_NON_IDEMPOTENT: ToolAnnotations = { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, +}; const WRITE_NON_IDEMPOTENT: ToolAnnotations = { readOnlyHint: false, destructiveHint: false, @@ -51,4 +57,21 @@ export const TOOL_ANNOTATIONS: Record = [MCP_TOOL_NAME.lockResume]: WRITE_IDEMPOTENT, [MCP_TOOL_NAME.unlockResume]: WRITE_IDEMPOTENT, [MCP_TOOL_NAME.getResumeStatistics]: READ_IDEMPOTENT, + [MCP_TOOL_NAME.listApplications]: READ_IDEMPOTENT, + [MCP_TOOL_NAME.readApplication]: READ_IDEMPOTENT, + [MCP_TOOL_NAME.listApplicationTags]: READ_IDEMPOTENT, + [MCP_TOOL_NAME.getApplicationStats]: READ_IDEMPOTENT, + [MCP_TOOL_NAME.createApplication]: WRITE_NON_IDEMPOTENT, + [MCP_TOOL_NAME.updateApplication]: WRITE_IDEMPOTENT, + [MCP_TOOL_NAME.addApplicationNote]: WRITE_NON_IDEMPOTENT, + [MCP_TOOL_NAME.deleteApplication]: WRITE_DESTRUCTIVE, + [MCP_TOOL_NAME.bulkUpdateApplications]: WRITE_IDEMPOTENT, + [MCP_TOOL_NAME.bulkDeleteApplications]: WRITE_DESTRUCTIVE, + [MCP_TOOL_NAME.importApplications]: WRITE_NON_IDEMPOTENT, + [MCP_TOOL_NAME.attachApplicationDocument]: WRITE_NON_IDEMPOTENT, + [MCP_TOOL_NAME.removeApplicationDocument]: WRITE_IDEMPOTENT, + [MCP_TOOL_NAME.autofillApplicationFromJob]: READ_OPEN_WORLD_NON_IDEMPOTENT, + [MCP_TOOL_NAME.scoreApplicationMatch]: WRITE_NON_IDEMPOTENT, + [MCP_TOOL_NAME.tailorResumeForApplication]: WRITE_NON_IDEMPOTENT, + [MCP_TOOL_NAME.draftApplicationMessage]: READ_NON_IDEMPOTENT, }; diff --git a/packages/mcp/src/tool-meta.ts b/packages/mcp/src/tool-meta.ts index bb3332ef4..70616deea 100644 --- a/packages/mcp/src/tool-meta.ts +++ b/packages/mcp/src/tool-meta.ts @@ -4,11 +4,71 @@ */ import z from "zod"; import { resumePatchOperationsSchema } from "@reactive-resume/ai/tools/resume-tool-contracts"; +import { applicationStatusSchema, contactSchema } from "@reactive-resume/schema/applications/data"; import { MCP_TOOL_NAME as T } from "./mcp-tool-names"; import { TOOL_ANNOTATIONS } from "./tool-annotations"; +const MAX_APPLICATION_DOCUMENT_BYTES = 10 * 1024 * 1024; + // ponytail: shared schema fragment; exported so server-card can re-use without re-importing const resumeIdSchema = z.string().min(1).describe(`Resume ID. Use \`${T.listResumes}\` to find valid IDs.`); +const applicationIdSchema = z + .string() + .min(1) + .describe(`Application ID. Use \`${T.listApplications}\` to find valid IDs.`); +const applicationDocumentKindSchema = z.enum(["resume", "cover-letter"]); +const httpUrlSchema = z + .string() + .trim() + .refine((value) => { + try { + const parsed = new URL(value); + return parsed.protocol === "http:" || parsed.protocol === "https:"; + } catch { + return false; + } + }, "URL must use http or https."); +const pdfBase64Schema = z + .string() + .min(1) + .refine((value) => Buffer.from(value, "base64").byteLength <= MAX_APPLICATION_DOCUMENT_BYTES, { + message: "Decoded PDF must be 10MB or smaller.", + }) + .describe("Base64-encoded PDF bytes. Only application/pdf documents up to 10MB are accepted."); + +const applicationMutableFieldsSchema = { + company: z.string().min(1).optional().describe("Company name."), + role: z.string().min(1).optional().describe("Role or job title."), + status: applicationStatusSchema.optional().describe("Pipeline stage."), + location: z.string().nullable().optional(), + salary: z.string().nullable().optional(), + source: z.string().nullable().optional(), + sourceUrl: httpUrlSchema.nullable().optional(), + jobDescription: z.string().max(20_000).nullable().optional(), + notes: z.string().nullable().optional(), + resumeId: z.string().nullable().optional(), + resumeFileUrl: z.string().nullable().optional(), + resumeFileName: z.string().nullable().optional(), + coverLetterUrl: z.string().nullable().optional(), + coverLetterName: z.string().nullable().optional(), + followUpAt: z + .string() + .datetime({ offset: true }) + .nullable() + .optional() + .describe("Follow-up timestamp in ISO 8601 format."), + followUpNote: z.string().nullable().optional(), + contacts: z.array(contactSchema).optional(), + tags: z.array(z.string()).optional(), +} as const; + +const createApplicationSchema = z + .object({ + ...applicationMutableFieldsSchema, + company: z.string().min(1).describe("Company name."), + role: z.string().min(1).describe("Role or job title."), + }) + .strict(); export const TOOL_META = { [T.listResumes]: { @@ -234,4 +294,131 @@ export const TOOL_META = { inputSchema: z.object({ id: resumeIdSchema }), annotations: TOOL_ANNOTATIONS[T.getResumeStatistics], }, + [T.listApplications]: { + title: "List Applications", + description: + "List job applications for the authenticated account. Use this before reading or updating existing applications.", + inputSchema: z.object({ + status: applicationStatusSchema.optional(), + tags: z.array(z.string()).optional().default([]), + includeArchived: z.boolean().optional().default(false), + }), + annotations: TOOL_ANNOTATIONS[T.listApplications], + }, + [T.readApplication]: { + title: "Read Application", + description: "Read one full job application, including contacts, document URLs, follow-up details, and timeline.", + inputSchema: z.object({ id: applicationIdSchema }), + annotations: TOOL_ANNOTATIONS[T.readApplication], + }, + [T.listApplicationTags]: { + title: "List Application Tags", + description: "Return every distinct tag used across job applications.", + inputSchema: z.object({}), + annotations: TOOL_ANNOTATIONS[T.listApplicationTags], + }, + [T.getApplicationStats]: { + title: "Get Application Stats", + description: "Return aggregate application counts by pipeline stage and source for insights.", + inputSchema: z.object({}), + annotations: TOOL_ANNOTATIONS[T.getApplicationStats], + }, + [T.createApplication]: { + title: "Create Application", + description: "Create a tracked job application. Company and role are required.", + inputSchema: createApplicationSchema, + annotations: TOOL_ANNOTATIONS[T.createApplication], + }, + [T.updateApplication]: { + title: "Update Application", + description: + "Update application fields, move stages, archive/unarchive, edit contacts, follow-up, tags, or linked resume.", + inputSchema: z.object({ + id: applicationIdSchema, + ...applicationMutableFieldsSchema, + archived: z.boolean().optional().describe("Whether the application is hidden from active views."), + }), + annotations: TOOL_ANNOTATIONS[T.updateApplication], + }, + [T.addApplicationNote]: { + title: "Add Application Note", + description: "Append a free-text note to an application's timeline.", + inputSchema: z.object({ id: applicationIdSchema, text: z.string().min(1) }), + annotations: TOOL_ANNOTATIONS[T.addApplicationNote], + }, + [T.deleteApplication]: { + title: "Delete Application", + description: "Permanently delete one job application and its owned uploaded documents.", + inputSchema: z.object({ id: applicationIdSchema }), + annotations: TOOL_ANNOTATIONS[T.deleteApplication], + }, + [T.bulkUpdateApplications]: { + title: "Bulk Update Applications", + description: "Move, archive/unarchive, or add tags to multiple applications.", + inputSchema: z.object({ + ids: z.array(z.string()).min(1), + status: applicationStatusSchema.optional(), + archived: z.boolean().optional(), + addTags: z.array(z.string()).optional(), + }), + annotations: TOOL_ANNOTATIONS[T.bulkUpdateApplications], + }, + [T.bulkDeleteApplications]: { + title: "Bulk Delete Applications", + description: "Permanently delete multiple applications.", + inputSchema: z.object({ ids: z.array(z.string()).min(1) }), + annotations: TOOL_ANNOTATIONS[T.bulkDeleteApplications], + }, + [T.importApplications]: { + title: "Import Applications", + description: "Bulk-create application rows parsed from CSV or another source. Maximum 500 items.", + inputSchema: z.object({ items: z.array(createApplicationSchema).min(1).max(500) }), + annotations: TOOL_ANNOTATIONS[T.importApplications], + }, + [T.attachApplicationDocument]: { + title: "Attach Application Document", + description: "Attach a sent resume or cover-letter PDF to an application using base64-encoded PDF bytes.", + inputSchema: z.object({ + id: applicationIdSchema, + kind: applicationDocumentKindSchema, + fileName: z.string().min(1), + contentType: z.literal("application/pdf"), + dataBase64: pdfBase64Schema, + }), + annotations: TOOL_ANNOTATIONS[T.attachApplicationDocument], + }, + [T.removeApplicationDocument]: { + title: "Remove Application Document", + description: "Remove a sent resume or cover-letter PDF from an application.", + inputSchema: z.object({ id: applicationIdSchema, kind: applicationDocumentKindSchema }), + annotations: TOOL_ANNOTATIONS[T.removeApplicationDocument], + }, + [T.autofillApplicationFromJob]: { + title: "Autofill Application From Job", + description: + "Use AI to extract company, role, location, salary, and job description from a job URL or pasted posting.", + inputSchema: z.object({ + sourceUrl: httpUrlSchema.optional(), + jobDescription: z.string().max(20_000).optional(), + }), + annotations: TOOL_ANNOTATIONS[T.autofillApplicationFromJob], + }, + [T.scoreApplicationMatch]: { + title: "Score Application Match", + description: "Score the linked resume against the application's job description and persist match metadata.", + inputSchema: z.object({ id: applicationIdSchema }), + annotations: TOOL_ANNOTATIONS[T.scoreApplicationMatch], + }, + [T.tailorResumeForApplication]: { + title: "Tailor Resume For Application", + description: "Create and link a tailored copy of the application's linked resume.", + inputSchema: z.object({ id: applicationIdSchema }), + annotations: TOOL_ANNOTATIONS[T.tailorResumeForApplication], + }, + [T.draftApplicationMessage]: { + title: "Draft Application Message", + description: "Draft either a cover letter or recruiter follow-up from application and resume context.", + inputSchema: z.object({ id: applicationIdSchema, kind: z.enum(["cover-letter", "follow-up"]) }), + annotations: TOOL_ANNOTATIONS[T.draftApplicationMessage], + }, } as const; diff --git a/packages/mcp/src/tools.test.ts b/packages/mcp/src/tools.test.ts index 6f95cf375..a0e4a8204 100644 --- a/packages/mcp/src/tools.test.ts +++ b/packages/mcp/src/tools.test.ts @@ -23,7 +23,7 @@ vi.mock("@reactive-resume/env/server", () => ({ const { MCP_TOOL_NAME, registerTools } = await import("./tools"); -type ToolHandler = (input: { id: string }) => Promise<{ +type ToolHandler = (input: Record) => Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean; }>; @@ -63,6 +63,27 @@ const clientMock = { setLocked: vi.fn(), statistics: { getById: vi.fn() }, }, + applications: { + list: vi.fn(), + getById: vi.fn(), + tags: vi.fn(), + stats: vi.fn(), + create: vi.fn(), + update: vi.fn(), + addNote: vi.fn(), + delete: vi.fn(), + bulkUpdate: vi.fn(), + bulkDelete: vi.fn(), + import: vi.fn(), + attachDocument: vi.fn(), + removeDocument: vi.fn(), + ai: { + autofill: vi.fn(), + matchScore: vi.fn(), + tailorResume: vi.fn(), + draftMessage: vi.fn(), + }, + }, }; describe("registerTools", () => { @@ -104,4 +125,123 @@ describe("registerTools", () => { it("keeps the tool name stable", () => { expect(MCP_TOOL_NAME.downloadResumePdf).toBe("download_resume_pdf"); }); + + it("registers application tracker tools", () => { + const { server, registered } = makeFakeServer(); + registerTools(server as never, clientMock as never, new Headers()); + + const names = registered.map((item) => item.name); + expect(names).toContain("list_applications"); + expect(names).toContain("create_application"); + expect(names).toContain("attach_application_document"); + expect(names).toContain("draft_application_message"); + }); + + it("lists applications as JSON", async () => { + clientMock.applications.list.mockResolvedValueOnce([{ id: "app-1", company: "Acme", role: "Engineer" }]); + const { server, registered } = makeFakeServer(); + registerTools(server as never, clientMock as never, new Headers()); + + const tool = registered.find((item) => item.name === "list_applications")!; + const result = await tool.handler({ includeArchived: true, tags: ["remote"] }); + + expect(clientMock.applications.list).toHaveBeenCalledWith({ includeArchived: true, tags: ["remote"] }); + expect(JSON.parse(result.content[0]!.text)).toEqual([{ id: "app-1", company: "Acme", role: "Engineer" }]); + }); + + it("creates applications through the router client", async () => { + clientMock.applications.create.mockResolvedValueOnce("app-1"); + const { server, registered } = makeFakeServer(); + registerTools(server as never, clientMock as never, new Headers()); + + const tool = registered.find((item) => item.name === "create_application")!; + const result = await tool.handler({ + company: "Acme", + role: "Engineer", + status: "saved", + followUpAt: "2026-07-10T09:30:00.000Z", + }); + + expect(clientMock.applications.create).toHaveBeenCalledWith({ + company: "Acme", + role: "Engineer", + status: "saved", + followUpAt: new Date("2026-07-10T09:30:00.000Z"), + }); + expect(JSON.parse(result.content[0]!.text)).toEqual({ id: "app-1" }); + }); + + it("updates applications with followUpAt coerced to Date", async () => { + clientMock.applications.update.mockResolvedValueOnce({ id: "app-1", company: "Acme" }); + const { server, registered } = makeFakeServer(); + registerTools(server as never, clientMock as never, new Headers()); + + const tool = registered.find((item) => item.name === "update_application")!; + await tool.handler({ id: "app-1", followUpAt: "2026-07-11T10:15:00.000Z" }); + + expect(clientMock.applications.update).toHaveBeenCalledWith({ + id: "app-1", + followUpAt: new Date("2026-07-11T10:15:00.000Z"), + }); + }); + + it("imports applications with followUpAt coerced to Date and null preserved", async () => { + clientMock.applications.import.mockResolvedValueOnce({ imported: 2 }); + const { server, registered } = makeFakeServer(); + registerTools(server as never, clientMock as never, new Headers()); + + const tool = registered.find((item) => item.name === "import_applications")!; + const result = await tool.handler({ + items: [ + { company: "Acme", role: "Engineer", followUpAt: "2026-07-12T12:00:00.000Z" }, + { company: "Beta", role: "Designer", followUpAt: null }, + ], + }); + + expect(clientMock.applications.import).toHaveBeenCalledWith({ + items: [ + { company: "Acme", role: "Engineer", followUpAt: new Date("2026-07-12T12:00:00.000Z") }, + { company: "Beta", role: "Designer", followUpAt: null }, + ], + }); + expect(JSON.parse(result.content[0]!.text)).toEqual({ imported: 2 }); + }); + + it("attaches a base64 PDF document through the router client", async () => { + clientMock.applications.attachDocument.mockResolvedValueOnce({ id: "app-1", resumeFileName: "resume.pdf" }); + const { server, registered } = makeFakeServer(); + registerTools(server as never, clientMock as never, new Headers()); + + const tool = registered.find((item) => item.name === "attach_application_document")!; + const result = await tool.handler({ + id: "app-1", + kind: "resume", + fileName: "resume.pdf", + contentType: "application/pdf", + dataBase64: Buffer.from("%PDF-1.4").toString("base64"), + }); + + const call = clientMock.applications.attachDocument.mock.calls[0]?.[0] as { file: File }; + expect(call.file).toBeInstanceOf(File); + expect(call.file.name).toBe("resume.pdf"); + expect(call.file.type).toBe("application/pdf"); + expect(JSON.parse(result.content[0]!.text)).toEqual({ id: "app-1", resumeFileName: "resume.pdf" }); + }); + + it("rejects non-PDF application document attachments before calling the client", async () => { + const { server, registered } = makeFakeServer(); + registerTools(server as never, clientMock as never, new Headers()); + + const tool = registered.find((item) => item.name === "attach_application_document")!; + const result = await tool.handler({ + id: "app-1", + kind: "resume", + fileName: "resume.txt", + contentType: "text/plain", + dataBase64: Buffer.from("hello").toString("base64"), + }); + + expect(result.isError).toBe(true); + expect(clientMock.applications.attachDocument).not.toHaveBeenCalled(); + }); }); diff --git a/packages/mcp/src/tools.ts b/packages/mcp/src/tools.ts index 2608634d7..613ae9c8b 100644 --- a/packages/mcp/src/tools.ts +++ b/packages/mcp/src/tools.ts @@ -4,6 +4,7 @@ import type { RouterClient } from "@orpc/server"; import type { resumePatchOperationsSchema } from "@reactive-resume/ai/tools/resume-tool-contracts"; import type router from "@reactive-resume/api/routers"; import type z from "zod"; +import { Buffer } from "node:buffer"; import { resolveUserFromRequestHeaders } from "@reactive-resume/api/context"; import { createResumePdfDownloadUrl } from "@reactive-resume/api/features/resume/export"; import { env } from "@reactive-resume/env/server"; @@ -57,6 +58,28 @@ function text(value: string): CallToolResult { return { content: [{ type: "text", text: value }] }; } +function json(value: unknown): CallToolResult { + return text(JSON.stringify(value, null, 2)); +} + +function fileFromBase64(input: { fileName: string; contentType: string; dataBase64: string }): File { + if (input.contentType !== "application/pdf") throw new Error("Application documents must be PDF files."); + + const bytes = Buffer.from(input.dataBase64, "base64"); + if (bytes.length === 0) throw new Error("Application document cannot be empty."); + + return new File([bytes], input.fileName, { type: input.contentType }); +} + +function coerceFollowUpAt(input: Record): Record { + if (!("followUpAt" in input)) return input; + + const followUpAt = input.followUpAt; + if (followUpAt === undefined || followUpAt === null || followUpAt instanceof Date) return input; + + return { ...input, followUpAt: new Date(String(followUpAt)) }; +} + function buildResumeShareUrl(username: string, slug: string): string { const base = env.APP_URL.replace(/\/$/, ""); return `${base}/${encodeURIComponent(username)}/${encodeURIComponent(slug)}`; @@ -339,4 +362,161 @@ export function registerTools(server: McpServer, client: RouterClient json(await client.applications.list(params as never))), + ); + + server.registerTool( + T.readApplication, + TOOL_META[T.readApplication], + withErrorHandling("reading application", async ({ id }: { id: string }) => + json(await client.applications.getById({ id })), + ), + ); + + server.registerTool( + T.listApplicationTags, + TOOL_META[T.listApplicationTags], + withErrorHandling("listing application tags", async () => json(await client.applications.tags())), + ); + + server.registerTool( + T.getApplicationStats, + TOOL_META[T.getApplicationStats], + withErrorHandling("getting application stats", async () => json(await client.applications.stats())), + ); + + server.registerTool( + T.createApplication, + TOOL_META[T.createApplication], + withErrorHandling("creating application", async (params) => { + const id = await client.applications.create(coerceFollowUpAt(params as Record) as never); + return json({ id }); + }), + ); + + server.registerTool( + T.updateApplication, + TOOL_META[T.updateApplication], + withErrorHandling("updating application", async (params) => { + return json(await client.applications.update(coerceFollowUpAt(params as Record) as never)); + }), + ); + + server.registerTool( + T.addApplicationNote, + TOOL_META[T.addApplicationNote], + withErrorHandling("adding application note", async ({ id, text: noteText }: { id: string; text: string }) => { + return json(await client.applications.addNote({ id, text: noteText })); + }), + ); + + server.registerTool( + T.deleteApplication, + TOOL_META[T.deleteApplication], + withErrorHandling("deleting application", async ({ id }: { id: string }) => { + await client.applications.delete({ id }); + return text(`Deleted application (${id}).`); + }), + ); + + server.registerTool( + T.bulkUpdateApplications, + TOOL_META[T.bulkUpdateApplications], + withErrorHandling("bulk updating applications", async (params) => + json(await client.applications.bulkUpdate(params as never)), + ), + ); + + server.registerTool( + T.bulkDeleteApplications, + TOOL_META[T.bulkDeleteApplications], + withErrorHandling("bulk deleting applications", async ({ ids }: { ids: string[] }) => { + return json(await client.applications.bulkDelete({ ids })); + }), + ); + + server.registerTool( + T.importApplications, + TOOL_META[T.importApplications], + withErrorHandling("importing applications", async (params) => { + const input = params as { items: Array> }; + return json( + await client.applications.import({ items: input.items.map((item) => coerceFollowUpAt(item)) } as never), + ); + }), + ); + + server.registerTool( + T.attachApplicationDocument, + TOOL_META[T.attachApplicationDocument], + withErrorHandling( + "attaching application document", + async ({ + id, + kind, + fileName, + contentType, + dataBase64, + }: { + id: string; + kind: "resume" | "cover-letter"; + fileName: string; + contentType: string; + dataBase64: string; + }) => { + const file = fileFromBase64({ fileName, contentType, dataBase64 }); + return json(await client.applications.attachDocument({ id, kind, file })); + }, + ), + ); + + server.registerTool( + T.removeApplicationDocument, + TOOL_META[T.removeApplicationDocument], + withErrorHandling( + "removing application document", + async ({ id, kind }: { id: string; kind: "resume" | "cover-letter" }) => { + return json(await client.applications.removeDocument({ id, kind })); + }, + ), + ); + + server.registerTool( + T.autofillApplicationFromJob, + TOOL_META[T.autofillApplicationFromJob], + withErrorHandling("autofilling application from job", async (params) => + json(await client.applications.ai.autofill(params as never)), + ), + ); + + server.registerTool( + T.scoreApplicationMatch, + TOOL_META[T.scoreApplicationMatch], + withErrorHandling("scoring application match", async ({ id }: { id: string }) => { + return json(await client.applications.ai.matchScore({ id })); + }), + ); + + server.registerTool( + T.tailorResumeForApplication, + TOOL_META[T.tailorResumeForApplication], + withErrorHandling("tailoring resume for application", async ({ id }: { id: string }) => { + return json(await client.applications.ai.tailorResume({ id })); + }), + ); + + server.registerTool( + T.draftApplicationMessage, + TOOL_META[T.draftApplicationMessage], + withErrorHandling( + "drafting application message", + async ({ id, kind }: { id: string; kind: "cover-letter" | "follow-up" }) => + json(await client.applications.ai.draftMessage({ id, kind })), + ), + ); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39129d5b6..1fdf02d4c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,6 +54,9 @@ importers: '@vitest/coverage-v8': specifier: ^4.1.9 version: 4.1.9(vitest@4.1.9) + github-actionlint: + specifier: 1.7.12 + version: 1.7.12 happy-dom: specifier: ^20.10.6 version: 20.10.6 @@ -63,6 +66,9 @@ importers: lefthook: specifier: ^2.1.9 version: 2.1.9 + markdownlint-cli2: + specifier: 0.23.0 + version: 0.23.0 pg: specifier: ^8.22.0 version: 8.22.0 @@ -2390,6 +2396,10 @@ packages: '@ioredis/commands@1.10.0': resolution: {integrity: sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==} + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + '@jest/schemas@29.6.3': resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4229,6 +4239,9 @@ packages: '@types/jsesc@2.5.1': resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} + '@types/katex@0.16.8': + resolution: {integrity: sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -4447,6 +4460,10 @@ packages: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} + adm-zip@0.5.18: + resolution: {integrity: sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==} + engines: {node: '>=12.0'} + ai@7.0.15: resolution: {integrity: sha512-7406MUy9O5sIhwOgxEWuoj+td3XUGgG96SBt6pmBU4t4sQ2fpnDx5UnHaXT2HUTXl5GorbWz/MfCrSPRz0QJqw==} engines: {node: '>=22'} @@ -4752,6 +4769,10 @@ packages: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + citty@0.2.2: resolution: {integrity: sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==} @@ -4848,6 +4869,10 @@ packages: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + conf@10.2.0: resolution: {integrity: sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==} engines: {node: '>=12'} @@ -5568,6 +5593,11 @@ packages: deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead. hasBin: true + github-actionlint@1.7.12: + resolution: {integrity: sha512-kSVz5clt9voatp2Tdh1BETKNooT358/nPChYseaDC4M6f9Hc/sBRMbhagx2FTu2F/WHzDoxlNPh2EqwBngTYQg==} + engines: {node: '>=18.0.0'} + hasBin: true + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -5584,6 +5614,10 @@ packages: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} + globby@16.2.0: + resolution: {integrity: sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q==} + engines: {node: '>=20'} + goober@2.1.19: resolution: {integrity: sha512-U7veizMqxyKlM58+Z5j2ngJBH/r9siDmxpvNxSw0PylF6WQvrASJEZrxh1hidRBJc2jqoBVSyOban5u8m+6Rxg==} peerDependencies: @@ -5681,6 +5715,10 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} @@ -5793,6 +5831,10 @@ packages: resolution: {integrity: sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==} engines: {node: '>=12'} + is-path-inside@4.0.0: + resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} + engines: {node: '>=12'} + is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} @@ -5902,6 +5944,10 @@ packages: resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} hasBin: true + js-yaml@5.2.0: + resolution: {integrity: sha512-YeLUMlvR4Ou1B119LIaM0r65JvbOBooJDc9yEu0dClb/uSC5P4FrLU8OCCz/HXWvtPoIrR0dRzABTjo1sTN9Bw==} + hasBin: true + jsbi@4.3.2: resolution: {integrity: sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==} @@ -5930,9 +5976,16 @@ packages: engines: {node: '>=6'} hasBin: true + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + jsonfile@6.2.1: resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + jsonpointer@5.0.1: + resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} + engines: {node: '>=0.10.0'} + jsonrepair@3.15.0: resolution: {integrity: sha512-wy8OTjwsJwQRnQJkKnMJJ9vcytRdBPAgIF/Hy6+s1dAj42BHMKiyL8JzEieIl3JY7idt8eyHwBWTO8mh/+mtwA==} hasBin: true @@ -5940,6 +5993,10 @@ packages: jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + katex@0.16.47: + resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} + hasBin: true + kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} @@ -6111,6 +6168,9 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + linkify-it@5.0.2: + resolution: {integrity: sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==} + linkifyjs@4.3.3: resolution: {integrity: sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==} @@ -6157,9 +6217,27 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + markdown-it@14.2.0: + resolution: {integrity: sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==} + hasBin: true + markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + markdownlint-cli2-formatter-default@0.0.6: + resolution: {integrity: sha512-VVDGKsq9sgzu378swJ0fcHfSicUnMxnL8gnLm/Q4J/xsNJ4e5bA6lvAz7PCzIl0/No0lHyaWdqVD2jotxOSFMQ==} + peerDependencies: + markdownlint-cli2: '>=0.0.4' + + markdownlint-cli2@0.23.0: + resolution: {integrity: sha512-1nmgQmU/ZTMRVwYCDs7i1HI3zfBISnT2NNRv+9V01oOLZbAtqL+a7tldpPhBWBVBten3FqhMCGV6EUh9McqutQ==} + engines: {node: '>=22'} + hasBin: true + + markdownlint@0.41.0: + resolution: {integrity: sha512-xMUI3ChBuRuxuLF4ENvCZyS8z/+Jly1coUcZwErKLIB3sDj7ojpaTBa1e9YVPhSN4jGEIjYGQCldbTJS/hqS+A==} + engines: {node: '>=22'} + marked@15.0.12: resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==} engines: {node: '>= 18'} @@ -6220,6 +6298,9 @@ packages: mdn-data@2.27.1: resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + media-engine@1.0.3: resolution: {integrity: sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg==} @@ -6249,6 +6330,9 @@ packages: micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + micromark-extension-directive@4.0.0: + resolution: {integrity: sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==} + micromark-extension-gfm-autolink-literal@2.1.0: resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} @@ -6270,6 +6354,9 @@ packages: micromark-extension-gfm@3.0.0: resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + micromark-extension-math@3.1.0: + resolution: {integrity: sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==} + micromark-factory-destination@2.0.1: resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} @@ -6380,6 +6467,10 @@ packages: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + moo@0.5.3: resolution: {integrity: sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA==} @@ -6876,6 +6967,10 @@ packages: engines: {node: '>=16.0.0'} hasBin: true + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + pvtsutils@1.3.6: resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==} @@ -7260,6 +7355,10 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + smol-toml@1.7.0: resolution: {integrity: sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==} engines: {node: '>= 18'} @@ -7441,6 +7540,10 @@ packages: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} + tar@7.5.19: + resolution: {integrity: sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==} + engines: {node: '>=18'} + throttleit@2.1.0: resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} engines: {node: '>=18'} @@ -7578,6 +7681,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + uint8array-extras@1.5.0: resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} engines: {node: '>=18'} @@ -7613,6 +7719,10 @@ packages: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} + unicorn-magic@0.4.0: + resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} + engines: {node: '>=20'} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -7905,6 +8015,10 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + yaml@1.10.3: resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} engines: {node: '>= 6'} @@ -9201,6 +9315,10 @@ snapshots: '@ioredis/commands@1.10.0': {} + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.3 + '@jest/schemas@29.6.3': dependencies: '@sinclair/typebox': 0.27.10 @@ -11071,6 +11189,8 @@ snapshots: '@types/jsesc@2.5.1': {} + '@types/katex@0.16.8': {} + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -11290,6 +11410,8 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 + adm-zip@0.5.18: {} + ai@7.0.15(zod@4.4.3): dependencies: '@ai-sdk/gateway': 4.0.12(zod@4.4.3) @@ -11550,6 +11672,8 @@ snapshots: dependencies: readdirp: 5.0.0 + chownr@3.0.0: {} + citty@0.2.2: {} cjk-regex@3.4.0: @@ -11635,6 +11759,8 @@ snapshots: commander@14.0.3: {} + commander@8.3.0: {} + conf@10.2.0: dependencies: ajv: 8.20.0 @@ -12243,6 +12369,11 @@ snapshots: - conventional-commits-filter - conventional-commits-parser + github-actionlint@1.7.12: + dependencies: + adm-zip: 0.5.18 + tar: 7.5.19 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -12259,6 +12390,15 @@ snapshots: globals@11.12.0: {} + globby@16.2.0: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + fast-glob: 3.3.3 + ignore: 7.0.5 + is-path-inside: 4.0.0 + slash: 5.1.0 + unicorn-magic: 0.4.0 + goober@2.1.19(csstype@3.2.3): dependencies: csstype: 3.2.3 @@ -12377,6 +12517,8 @@ snapshots: ignore@5.3.2: {} + ignore@7.0.5: {} + immediate@3.0.6: {} immer@11.1.11: {} @@ -12460,6 +12602,8 @@ snapshots: is-obj@3.0.0: {} + is-path-inside@4.0.0: {} + is-plain-obj@4.1.0: {} is-promise@4.0.0: {} @@ -12542,6 +12686,10 @@ snapshots: dependencies: argparse: 2.0.1 + js-yaml@5.2.0: + dependencies: + argparse: 2.0.1 + jsbi@4.3.2: {} jsesc@3.1.0: {} @@ -12558,12 +12706,16 @@ snapshots: json5@2.2.3: {} + jsonc-parser@3.3.1: {} + jsonfile@6.2.1: dependencies: universalify: 2.0.1 optionalDependencies: graceful-fs: 4.2.11 + jsonpointer@5.0.1: {} + jsonrepair@3.15.0: {} jszip@3.10.1: @@ -12573,6 +12725,10 @@ snapshots: readable-stream: 2.3.8 setimmediate: 1.0.5 + katex@0.16.47: + dependencies: + commander: 8.3.0 + kleur@3.0.3: {} kleur@4.1.5: {} @@ -12711,6 +12867,10 @@ snapshots: lines-and-columns@1.2.4: {} + linkify-it@5.0.2: + dependencies: + uc.micro: 2.1.0 + linkifyjs@4.3.3: {} locate-path@3.0.0: @@ -12758,8 +12918,49 @@ snapshots: dependencies: semver: 7.8.5 + markdown-it@14.2.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.2 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + markdown-table@3.0.4: {} + markdownlint-cli2-formatter-default@0.0.6(markdownlint-cli2@0.23.0): + dependencies: + markdownlint-cli2: 0.23.0 + + markdownlint-cli2@0.23.0: + dependencies: + globby: 16.2.0 + js-yaml: 5.2.0 + jsonc-parser: 3.3.1 + jsonpointer: 5.0.1 + markdown-it: 14.2.0 + markdownlint: 0.41.0 + markdownlint-cli2-formatter-default: 0.0.6(markdownlint-cli2@0.23.0) + micromatch: 4.0.8 + smol-toml: 1.7.0 + transitivePeerDependencies: + - supports-color + + markdownlint@0.41.0: + dependencies: + micromark: 4.0.2 + micromark-core-commonmark: 2.0.3 + micromark-extension-directive: 4.0.0 + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-math: 3.1.0 + micromark-util-types: 2.0.2 + string-width: 8.2.1 + transitivePeerDependencies: + - supports-color + marked@15.0.12: {} math-intrinsics@1.1.0: {} @@ -12921,6 +13122,8 @@ snapshots: mdn-data@2.27.1: {} + mdurl@2.0.0: {} + media-engine@1.0.3: {} media-typer@1.1.0: {} @@ -12954,6 +13157,16 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 + micromark-extension-directive@4.0.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + parse-entities: 4.0.2 + micromark-extension-gfm-autolink-literal@2.1.0: dependencies: micromark-util-character: 2.1.1 @@ -13012,6 +13225,16 @@ snapshots: micromark-util-combine-extensions: 2.0.1 micromark-util-types: 2.0.2 + micromark-extension-math@3.1.0: + dependencies: + '@types/katex': 0.16.8 + devlop: 1.1.0 + katex: 0.16.47 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + micromark-factory-destination@2.0.1: dependencies: micromark-util-character: 2.1.1 @@ -13161,6 +13384,10 @@ snapshots: minipass@7.1.3: {} + minizlib@3.1.0: + dependencies: + minipass: 7.1.3 + moo@0.5.3: {} motion-dom@12.42.2: @@ -13740,6 +13967,8 @@ snapshots: dependencies: commander: 10.0.1 + punycode.js@2.3.1: {} + pvtsutils@1.3.6: dependencies: tslib: 2.8.1 @@ -14265,6 +14494,8 @@ snapshots: sisteransi@1.0.5: {} + slash@5.1.0: {} + smol-toml@1.7.0: {} socket.io-adapter@2.5.8: @@ -14435,6 +14666,14 @@ snapshots: tapable@2.3.3: {} + tar@7.5.19: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.3 + minizlib: 3.1.0 + yallist: 5.0.0 + throttleit@2.1.0: {} tiny-inflate@1.0.3: {} @@ -14555,6 +14794,8 @@ snapshots: typescript@6.0.3: {} + uc.micro@2.1.0: {} + uint8array-extras@1.5.0: {} unbash@4.0.2: {} @@ -14586,6 +14827,8 @@ snapshots: unicorn-magic@0.3.0: {} + unicorn-magic@0.4.0: {} + unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -14796,6 +15039,8 @@ snapshots: yallist@3.1.1: {} + yallist@5.0.0: {} + yaml@1.10.3: {} yaml@2.9.0: {} diff --git a/skills/resume-builder/SKILL.md b/skills/resume-builder/SKILL.md index a4ba215e6..1488714b4 100644 --- a/skills/resume-builder/SKILL.md +++ b/skills/resume-builder/SKILL.md @@ -16,6 +16,18 @@ Build professional resumes through conversational AI for [Reactive Resume](https ## Workflow +### MCP Application Tracking + +Reactive Resume MCP can manage job applications as well as resumes. When the user wants to track applications through an agent, use the application tools instead of asking them to open the web UI. + +- Use `list_applications` before changing existing records. +- Use `create_application` for one new opportunity or `import_applications` for spreadsheet/CSV rows. +- Use `update_application` to move stages, archive/unarchive, edit contacts, set follow-ups, link a resume, or update job details. +- Use `add_application_note` to log timeline activity. +- Use `attach_application_document` and `remove_application_document` for sent resume or cover-letter PDFs. +- Use `score_application_match`, `tailor_resume_for_application`, and `draft_application_message` for Application Copilot workflows after a linked resume and job description exist. +- Review AI-generated cover letters and follow-ups before sending them. + ### Step 1: Gather Basic Information Ask for essential details first, unless the user has already provided them: