Merge branch 'main' of github.com:amruthpillai/reactive-resume

This commit is contained in:
Amruth Pillai
2026-05-07 15:14:21 +02:00
1015 changed files with 106087 additions and 141872 deletions
+36
View File
@@ -0,0 +1,36 @@
{
"name": "@reactive-resume/ai",
"version": "0.0.0",
"type": "module",
"private": true,
"exports": {
"./store": "./src/store.ts",
"./types": "./src/types.ts",
"./prompts": "./src/prompts.ts",
"./tools/patch-resume": "./src/tools/patch-resume.ts",
"./resume/extraction-template": "./src/resume/extraction-template.ts",
"./resume/sanitize": "./src/resume/sanitize.ts"
},
"scripts": {
"typecheck": "tsgo --noEmit",
"test": "vitest run --passWithNoTests",
"test:coverage": "vitest run --coverage --passWithNoTests",
"test:ci": "vitest run --coverage --reporter=default --reporter=github-actions --reporter=json --reporter=junit --outputFile.json=reports/vitest-results.json --outputFile.junit=reports/vitest-junit.xml --passWithNoTests",
"test:agent": "vitest run --reporter=agent --reporter=json --outputFile.json=reports/vitest-results.json --passWithNoTests"
},
"dependencies": {
"@reactive-resume/schema": "workspace:*",
"@reactive-resume/utils": "workspace:*",
"deepmerge-ts": "^7.1.5",
"fast-json-patch": "^3.1.1",
"immer": "^11.1.7",
"jsonrepair": "^3.14.0",
"zod": "^4.4.3",
"zustand": "^5.0.13"
},
"devDependencies": {
"@reactive-resume/config": "workspace:*",
"@typescript/native-preview": "7.0.0-dev.20260507.1",
"typescript": "^6.0.3"
}
}
+4
View File
@@ -0,0 +1,4 @@
declare module "*.md?raw" {
const content: string;
export default content;
}
+17
View File
@@ -0,0 +1,17 @@
/// <reference path="./markdown-raw.d.ts" />
import analyzeResumeSystemPrompt from "./prompts/analyze-resume-system.md?raw";
import chatSystemPromptTemplate from "./prompts/chat-system.md?raw";
import docxParserSystemPrompt from "./prompts/docx-parser-system.md?raw";
import docxParserUserPrompt from "./prompts/docx-parser-user.md?raw";
import pdfParserSystemPrompt from "./prompts/pdf-parser-system.md?raw";
import pdfParserUserPrompt from "./prompts/pdf-parser-user.md?raw";
export {
analyzeResumeSystemPrompt,
chatSystemPromptTemplate,
docxParserSystemPrompt,
docxParserUserPrompt,
pdfParserSystemPrompt,
pdfParserUserPrompt,
};
@@ -0,0 +1,65 @@
You are a senior resume reviewer and ATS optimization specialist.
Your task is to analyze the resume JSON provided in context and return a structured analysis.
## Core Objectives
Evaluate the resume for:
- clarity and specificity
- impact and quantification
- ATS compatibility
- structure and completeness
- language quality and relevance
## Strict Output Contract
Return only a JSON object that matches this exact structure:
{
"overallScore": 0-100 integer,
"scorecard": [
{
"dimension": "string",
"score": 0-100 integer,
"rationale": "string"
}
],
"suggestions": [
{
"title": "string",
"impact": "high" | "medium" | "low",
"why": "string",
"exampleRewrite": "string or null",
"copyPrompt": "string"
}
],
"strengths": ["string"]
}
Do not include markdown, comments, or additional keys.
## Evaluation Rules
1. Use 0-100 scoring for each dimension and overall score.
2. Keep rationales concise, specific, and evidence-based from resume content.
3. Suggestions must be prioritized by impact and be actionable.
4. Never invent candidate achievements or facts.
5. If data is missing, call it out explicitly in rationale/suggestions.
6. Keep scorecard dimensions practical and common for resume review.
## Suggestions Requirements
Each suggestion must include:
- a clear title
- impact level (`high`, `medium`, or `low`)
- explanation of why it matters
- a copyable prompt for improving that area in another LLM
`copyPrompt` should be concrete and directly usable, for example:
"Rewrite my experience bullets to emphasize measurable outcomes and ATS keywords. Keep each bullet under 25 words and include a metric where possible. Here is my current section: "
## Tone
Professional, direct, and constructive. Focus on helping the user improve quickly.
+54
View File
@@ -0,0 +1,54 @@
You are a resume editing assistant that must modify resume data only through JSON Patch (RFC 6902) tool calls.
## Objective
- Help the user improve resume content and structure.
- Apply edits safely and minimally via the `patch_resume` tool.
## Allowed Inputs
- User instructions in conversation.
- Current resume JSON state provided below.
## Hard Constraints
1. For any data change, always call `patch_resume`. Do not output raw patch arrays directly in chat text.
2. Generate the minimal set of patch operations required for the request.
3. Preserve existing data unless the user explicitly asks to replace or remove it.
4. Ask for confirmation before destructive edits (deletions, clears, or replacing large sections).
5. Stay resume-focused; decline off-topic requests.
6. Do not fabricate factual user history. For drafted content, label it as a draft and ask for confirmation.
7. Keep all paths and operations valid for RFC 6902 and current schema.
8. New item IDs must be UUIDs in `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx` format.
9. HTML fields (such as summary/description) must use valid HTML (`<p>`, `<ul>`, `<li>`, `<strong>`, `<em>` as needed).
## Conflict Resolution Order
1. Data safety and schema validity
2. User intent from latest instruction
3. Minimal-change editing strategy
## Editing Rules
- Prefer targeted `replace` operations over broad object replacements.
- Use `add` at `/items/-` for appending list entries.
- Use `remove` only when explicitly requested or confirmed.
- Keep `website` objects shaped as `{ "url": string, "label": string }`.
- Keep `hidden` fields explicit booleans.
## Resume Shape Reference
- Top-level keys: `basics`, `summary`, `picture`, `sections`, `customSections`, `metadata`
- Section item families in `sections`: `profiles`, `experience`, `education`, `projects`, `skills`, `languages`, `interests`, `awards`, `certifications`, `publications`, `volunteer`, `references`
## Output Contract
- If a change is needed: call `patch_resume`, then provide a concise natural-language confirmation.
- If no change is needed: provide concise guidance without tool calls.
- Never include markdown code blocks for patch payloads in your chat reply.
## Current Resume Data
```json
{{RESUME_DATA}}
```
@@ -0,0 +1,54 @@
You are a strict resume extraction engine for Microsoft Word files (DOC/DOCX). Convert the attached document into a Reactive Resume JSON object.
## Objective
- Extract resume content accurately and map it into the provided JSON template.
- Prioritize source fidelity and schema correctness over completeness.
## Allowed Input
- Use only visible, intended content from the attached document.
- Ignore hidden text, comments, track changes, revision history, document metadata, and layout artifacts.
## Hard Constraints
1. Extract only explicitly stated information.
2. Never fabricate, infer, or normalize missing data.
3. Keep original wording and original language.
4. When uncertain, omit content and leave template defaults.
5. Do not use external knowledge.
## Conflict Resolution Order
1. Schema validity (must return valid JSON matching template shape)
2. Source fidelity (exactly what the document states)
3. Omit uncertain values (never guess)
## Extraction Rules
- Dates: preserve exactly as written.
- URLs: include only URLs explicitly visible in document content.
- Contact data: copy as-is; do not reformat.
- Skills: include only explicit skill mentions.
- Descriptions: output HTML using `<p>`, `<ul>`, `<li>` while preserving meaning.
- Lists and tables: extract visible text faithfully; preserve relationships in section fields.
- Headers/footers: include only if they contain real resume data.
- IDs: generate unique UUIDs for all `id` fields.
- `hidden`: default to `false` unless explicitly indicated otherwise.
- `columns`: default to `1` unless clearly multi-column by content intent.
- `website`: when missing, use `{ "url": "", "label": "" }`.
## Section Mapping
- `basics`, `summary`, `experience`, `education`, `skills`, `projects`, `certifications`, `awards`, `languages`, `volunteer`, `publications`, `references`, `profiles`, `interests`
- Map based on explicit headings first; use local context only when heading is absent.
## Fallback Rules
- If the document is malformed or partially unreadable, return best-effort extraction for readable parts only.
- Keep unknown fields empty according to the template.
## Output Contract
- Return only one raw JSON object.
- No markdown, no commentary, no extra keys.
@@ -0,0 +1,3 @@
The Microsoft Word resume file is attached as a file in this message.
Process the attached document and return the final JSON object only, strictly following the system rules and the provided schema template.
@@ -0,0 +1,52 @@
You are a strict resume extraction engine for PDF files. Convert the attached PDF into a Reactive Resume JSON object.
## Objective
- Extract resume content accurately and map it into the provided JSON template.
- Prioritize source fidelity and schema correctness over completeness.
## Allowed Input
- Use only the visible content from the attached PDF document.
- Ignore OCR noise, watermarks, repeated headers/footers, and broken line wraps.
## Hard Constraints
1. Extract only explicitly stated information.
2. Never fabricate, infer, or normalize missing data.
3. Keep original wording and original language.
4. When uncertain, omit content and leave template defaults.
5. Do not use external knowledge.
## Conflict Resolution Order
1. Schema validity (must return valid JSON matching template shape)
2. Source fidelity (exactly what the PDF states)
3. Omit uncertain values (never guess)
## Extraction Rules
- Dates: preserve exactly as written.
- URLs: include only full URLs that are explicitly present.
- Contact data: copy as-is; do not reformat.
- Skills: include only explicit skill mentions.
- Descriptions: output HTML using `<p>`, `<ul>`, `<li>` while preserving meaning.
- IDs: generate unique UUIDs for all `id` fields.
- `hidden`: default to `false` unless explicitly indicated otherwise.
- `columns`: default to `1` unless clearly multi-column by content intent.
- `website`: when missing, use `{ "url": "", "label": "" }`.
## Section Mapping
- `basics`, `summary`, `experience`, `education`, `skills`, `projects`, `certifications`, `awards`, `languages`, `volunteer`, `publications`, `references`, `profiles`, `interests`
- Map based on explicit headings first; use local context only when heading is absent.
## Fallback Rules
- If the PDF is low quality or partially unreadable, return best-effort extraction for readable parts only.
- Keep unknown fields empty according to the template.
## Output Contract
- Return only one raw JSON object.
- No markdown, no commentary, no extra keys.
@@ -0,0 +1,3 @@
The resume PDF is attached as a file in this message.
Process the attached document and return the final JSON object only, strictly following the system rules and the provided schema template.
@@ -0,0 +1,132 @@
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
function makeEmptyItem(shape: Record<string, unknown>): Record<string, unknown> {
const item: Record<string, unknown> = {};
for (const [key, value] of Object.entries(shape)) {
if (typeof value === "string") item[key] = "";
else if (typeof value === "number") item[key] = 0;
else if (typeof value === "boolean") item[key] = false;
else if (Array.isArray(value)) item[key] = [];
else if (value !== null && typeof value === "object") {
item[key] = makeEmptyItem(value as Record<string, unknown>);
} else {
item[key] = "";
}
}
return item;
}
const SECTION_ITEM_SHAPES: Record<string, Record<string, unknown>> = {
profiles: {
id: "",
hidden: false,
icon: "",
network: "",
username: "",
website: { url: "", label: "", inlineLink: false },
},
experience: {
id: "",
hidden: false,
company: "",
position: "",
location: "",
period: "",
website: { url: "", label: "", inlineLink: false },
description: "",
},
education: {
id: "",
hidden: false,
school: "",
degree: "",
area: "",
grade: "",
location: "",
period: "",
website: { url: "", label: "", inlineLink: false },
description: "",
},
projects: {
id: "",
hidden: false,
name: "",
period: "",
website: { url: "", label: "", inlineLink: false },
description: "",
},
skills: { id: "", hidden: false, icon: "", name: "", proficiency: "", level: 0, keywords: [] },
languages: { id: "", hidden: false, language: "", fluency: "", level: 0 },
interests: { id: "", hidden: false, icon: "", name: "", keywords: [] },
awards: {
id: "",
hidden: false,
title: "",
awarder: "",
date: "",
website: { url: "", label: "", inlineLink: false },
description: "",
},
certifications: {
id: "",
hidden: false,
title: "",
issuer: "",
date: "",
website: { url: "", label: "", inlineLink: false },
description: "",
},
publications: {
id: "",
hidden: false,
title: "",
publisher: "",
date: "",
website: { url: "", label: "", inlineLink: false },
description: "",
},
volunteer: {
id: "",
hidden: false,
organization: "",
location: "",
period: "",
website: { url: "", label: "", inlineLink: false },
description: "",
},
references: {
id: "",
hidden: false,
name: "",
position: "",
website: { url: "", label: "", inlineLink: false },
phone: "",
description: "",
},
};
export function buildAiExtractionTemplate() {
const sections: Record<string, unknown> = {};
for (const [key, shape] of Object.entries(SECTION_ITEM_SHAPES)) {
const sectionKey = key as keyof typeof defaultResumeData.sections;
sections[key] = {
...defaultResumeData.sections[sectionKey],
items: [makeEmptyItem(shape)],
};
}
return {
...defaultResumeData,
basics: {
...defaultResumeData.basics,
customFields: [{ id: "", icon: "", text: "", link: "" }],
},
sections: {
...defaultResumeData.sections,
...sections,
},
};
}
+262
View File
@@ -0,0 +1,262 @@
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import { deepmergeCustom } from "deepmerge-ts";
import { jsonrepair } from "jsonrepair";
import { flattenError, ZodError } from "zod";
import { resumeDataSchema } from "@reactive-resume/schema/resume/data";
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
import { isObject } from "@reactive-resume/utils/sanitize";
import { buildAiExtractionTemplate } from "./extraction-template";
const aiExtractionTemplate = buildAiExtractionTemplate();
type SectionKey = keyof typeof sectionRequiredFieldMap;
type CoercionEntry = {
path: string;
fromType: string;
toType: string;
};
type DroppedSectionItemEntry = {
section: SectionKey;
index: number;
reason: string;
};
export type ResumeSanitizationDiagnostics = {
coercions: CoercionEntry[];
droppedSectionItems: DroppedSectionItemEntry[];
salvageApplied: boolean;
};
export type ResumeSanitizationResult = {
data: ResumeData;
diagnostics: ResumeSanitizationDiagnostics;
};
const mergeDefaultsDeep = deepmergeCustom({
filterValues: (values) => values.filter((value) => value !== undefined && value !== null),
mergeArrays: false,
});
const sectionRequiredFieldMap = {
profiles: "network",
experience: "company",
education: "school",
projects: "name",
skills: "name",
languages: "language",
interests: "name",
awards: "title",
certifications: "title",
publications: "title",
volunteer: "organization",
references: "name",
} as const;
function getValueType(value: unknown): string {
if (value === null) return "null";
if (Array.isArray(value)) return "array";
return typeof value;
}
function mergeDefaults<T extends Record<string, unknown>, S extends Record<string, unknown>>(
target: T,
source: S,
): T & S {
if (!isObject(target) || !isObject(source)) {
return (source !== undefined && source !== null ? source : target) as T & S;
}
return mergeDefaultsDeep(target, source) as T & S;
}
function coerceBoolean(value: unknown): boolean | undefined {
if (typeof value === "boolean") return value;
if (typeof value === "number") {
if (value === 1) return true;
if (value === 0) return false;
return;
}
if (typeof value !== "string") return;
const normalized = value.trim().toLowerCase();
if (normalized === "true" || normalized === "1") return true;
if (normalized === "false" || normalized === "0") return false;
return;
}
function coerceNumber(value: unknown): number | undefined {
if (typeof value === "number") return Number.isFinite(value) ? value : undefined;
if (typeof value !== "string") return;
const normalized = value.trim();
if (normalized.length === 0) return;
const coerced = Number(normalized);
return Number.isFinite(coerced) ? coerced : undefined;
}
function coerceValueAgainstTemplate(
value: unknown,
template: unknown,
path: string,
diagnostics: ResumeSanitizationDiagnostics,
): unknown {
if (value === undefined || value === null) return value;
if (template === undefined || template === null) return value;
if (typeof template === "boolean") {
const coerced = coerceBoolean(value);
if (coerced === undefined) return value;
if (coerced === value) return value;
diagnostics.coercions.push({ path, fromType: getValueType(value), toType: "boolean" });
return coerced;
}
if (typeof template === "number") {
const coerced = coerceNumber(value);
if (coerced === undefined) return value;
if (coerced === value) return value;
diagnostics.coercions.push({ path, fromType: getValueType(value), toType: "number" });
return coerced;
}
if (Array.isArray(template) && Array.isArray(value)) {
const itemTemplate = template[0];
if (itemTemplate === undefined) return value;
return value.map((entry, index) =>
coerceValueAgainstTemplate(entry, itemTemplate, `${path}[${index}]`, diagnostics),
);
}
if (!isObject(template) || !isObject(value)) return value;
const output: Record<string, unknown> = { ...value };
for (const key of Object.keys(template)) {
if (!(key in output)) continue;
const nextPath = path.length > 0 ? `${path}.${key}` : key;
output[key] = coerceValueAgainstTemplate(output[key], template[key], nextPath, diagnostics);
}
return output;
}
function getJsonBoundaryIndices(value: string): { first: number; last: number } {
const firstCurly = value.indexOf("{");
const firstSquare = value.indexOf("[");
const lastCurly = value.lastIndexOf("}");
const lastSquare = value.lastIndexOf("]");
let first = -1;
if (firstCurly !== -1 && firstSquare !== -1) {
first = Math.min(firstCurly, firstSquare);
} else {
first = Math.max(firstCurly, firstSquare);
}
return { first, last: Math.max(lastCurly, lastSquare) };
}
function normalizeResumeDataForSchema(data: Record<string, unknown>, diagnostics: ResumeSanitizationDiagnostics) {
if (!isObject(data)) return data;
if (!isObject(data.sections)) return data;
const normalizedSections: Record<string, unknown> = { ...data.sections };
for (const sectionKey of Object.keys(sectionRequiredFieldMap) as SectionKey[]) {
const section = normalizedSections[sectionKey];
if (!isObject(section)) continue;
if (!Array.isArray(section.items)) continue;
const itemTemplate = aiExtractionTemplate.sections[sectionKey].items[0] as Record<string, unknown>;
const requiredField = sectionRequiredFieldMap[sectionKey];
const normalizedItems = section.items
.filter((item): item is Record<string, unknown> => isObject(item))
.map((item) => {
const mergedItem = mergeDefaults(itemTemplate, item);
return coerceValueAgainstTemplate(
mergedItem,
itemTemplate,
`sections.${sectionKey}.items`,
diagnostics,
) as Record<string, unknown>;
})
.filter((item, index) => {
const requiredValue = item[requiredField];
if (typeof requiredValue !== "string" || requiredValue.trim().length === 0) {
diagnostics.salvageApplied = true;
diagnostics.droppedSectionItems.push({
section: sectionKey,
index,
reason: `missing required "${requiredField}"`,
});
return false;
}
return true;
})
.map((item) => {
const normalizedItem = { ...item };
if (typeof normalizedItem.id !== "string" || normalizedItem.id.trim().length === 0) {
diagnostics.salvageApplied = true;
normalizedItem.id = crypto.randomUUID();
}
if (typeof normalizedItem.hidden !== "boolean") {
diagnostics.salvageApplied = true;
normalizedItem.hidden = false;
}
return normalizedItem;
});
normalizedSections[sectionKey] = { ...section, items: normalizedItems };
}
return { ...data, sections: normalizedSections };
}
export function sanitizeAndParseResumeJson(resultText: string): ResumeSanitizationResult {
let jsonString = resultText;
const { first, last } = getJsonBoundaryIndices(jsonString);
if (first !== -1 && last !== -1 && last >= first) {
jsonString = jsonString.substring(first, last + 1);
}
try {
const diagnostics: ResumeSanitizationDiagnostics = {
coercions: [],
droppedSectionItems: [],
salvageApplied: false,
};
const repairedJson = jsonrepair(jsonString);
const parsedJson = JSON.parse(repairedJson);
const mergedData = mergeDefaults(defaultResumeData, parsedJson);
const coercedData = coerceValueAgainstTemplate(mergedData, defaultResumeData, "", diagnostics);
const normalizedData = normalizeResumeDataForSchema(coercedData as Record<string, unknown>, diagnostics);
const data = resumeDataSchema.parse({
...normalizedData,
customSections: [],
picture: defaultResumeData.picture,
metadata: defaultResumeData.metadata,
});
return { data, diagnostics };
} catch (error: unknown) {
if (error instanceof ZodError) {
console.error("Zod validation failed during resume parsing:", flattenError(error));
throw error;
}
console.error("Unknown error during resume data validation:", error);
throw new Error("An unknown error occurred while validating the merged resume data.");
}
}
+88
View File
@@ -0,0 +1,88 @@
import type { WritableDraft } from "immer";
import type { AIProvider } from "./types";
import { createJSONStorage, persist } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
import { create } from "zustand/react";
type TestStatus = "unverified" | "success" | "failure";
type AIStoreState = {
enabled: boolean;
provider: AIProvider;
model: string;
apiKey: string;
baseURL: string;
testStatus: TestStatus;
};
type AIStoreActions = {
canEnable: () => boolean;
setEnabled: (value: boolean) => void;
set: (fn: (draft: WritableDraft<AIStoreState>) => void) => void;
reset: () => void;
};
type AIStore = AIStoreState & AIStoreActions;
const initialState: AIStoreState = {
enabled: false,
provider: "openai",
model: "",
apiKey: "",
baseURL: "",
testStatus: "unverified",
};
export const useAIStore = create<AIStore>()(
persist(
immer((set, get) => ({
...initialState,
set: (fn) => {
set((draft) => {
const prev = {
provider: draft.provider,
model: draft.model,
apiKey: draft.apiKey,
baseURL: draft.baseURL,
};
fn(draft);
if (
draft.provider !== prev.provider ||
draft.model !== prev.model ||
draft.apiKey !== prev.apiKey ||
draft.baseURL !== prev.baseURL
) {
draft.testStatus = "unverified";
draft.enabled = false;
}
});
},
reset: () => set(() => initialState),
canEnable: () => {
const { testStatus } = get();
return testStatus === "success";
},
setEnabled: (value: boolean) => {
const canEnable = get().canEnable();
if (value && !canEnable) return;
set((draft) => {
draft.enabled = value;
});
},
})),
{
name: "ai-store",
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({
enabled: state.enabled,
provider: state.provider,
model: state.model,
apiKey: state.apiKey,
baseURL: state.baseURL,
testStatus: state.testStatus,
}),
},
),
);
+22
View File
@@ -0,0 +1,22 @@
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import type { Operation } from "fast-json-patch";
import z from "zod";
import { applyResumePatches, jsonPatchOperationSchema } from "@reactive-resume/utils/resume/patch";
export const patchResumeInputSchema = z.object({
operations: z
.array(jsonPatchOperationSchema)
.min(1)
.describe("Array of JSON Patch (RFC 6902) operations to apply to the resume"),
});
export const patchResumeDescription = `Apply JSON Patch (RFC 6902) operations to modify the user's resume data.
Use this tool whenever the user asks to change, add, or remove content from their resume.
Always generate the minimal set of operations needed. Prefer "replace" for updates, "add" for new content, "remove" for deletions.
Use the special "-" index to append to arrays (e.g. "/sections/experience/items/-").`;
export function executePatchResume(resumeData: ResumeData, operations: Operation[]) {
// Validates operations structurally and against the schema; throws on invalid
applyResumePatches(resumeData, operations);
return { success: true as const, appliedOperations: operations };
}
+16
View File
@@ -0,0 +1,16 @@
import { z } from "zod";
const AI_PROVIDERS = ["openai", "anthropic", "gemini", "vercel-ai-gateway", "openrouter", "ollama"] as const;
export type AIProvider = (typeof AI_PROVIDERS)[number];
export const aiProviderSchema = z.enum(AI_PROVIDERS);
export const AI_PROVIDER_DEFAULT_BASE_URLS: Record<AIProvider, string> = {
openai: "https://api.openai.com/v1",
anthropic: "https://api.anthropic.com/v1",
gemini: "https://generativelanguage.googleapis.com/v1beta",
"vercel-ai-gateway": "https://ai-gateway.vercel.sh/v3/ai",
openrouter: "https://openrouter.ai/api/v1",
ollama: "https://ollama.com/api",
};
+6
View File
@@ -0,0 +1,6 @@
{
"extends": "@reactive-resume/config/tsconfig.base.json",
"compilerOptions": {
"lib": ["ESNext", "DOM"]
}
}
+7
View File
@@ -0,0 +1,7 @@
import { fileURLToPath } from "node:url";
import { createVitestProjectConfig } from "../../vitest.shared";
export default createVitestProjectConfig({
name: "@reactive-resume/ai",
dirname: fileURLToPath(new URL(".", import.meta.url)),
});