careerclaw-js 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +362 -0
- package/README.md +348 -0
- package/SECURITY.md +156 -0
- package/SKILL.md +463 -0
- package/dist/adapters/hackernews.d.ts +36 -0
- package/dist/adapters/hackernews.d.ts.map +1 -0
- package/dist/adapters/hackernews.js +164 -0
- package/dist/adapters/hackernews.js.map +1 -0
- package/dist/adapters/index.d.ts +10 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +9 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/remoteok.d.ts +35 -0
- package/dist/adapters/remoteok.d.ts.map +1 -0
- package/dist/adapters/remoteok.js +212 -0
- package/dist/adapters/remoteok.js.map +1 -0
- package/dist/briefing.d.ts +81 -0
- package/dist/briefing.d.ts.map +1 -0
- package/dist/briefing.js +152 -0
- package/dist/briefing.js.map +1 -0
- package/dist/cli.d.ts +22 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +235 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +91 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +126 -0
- package/dist/config.js.map +1 -0
- package/dist/core/text-processing.d.ts +62 -0
- package/dist/core/text-processing.d.ts.map +1 -0
- package/dist/core/text-processing.js +187 -0
- package/dist/core/text-processing.js.map +1 -0
- package/dist/drafting.d.ts +28 -0
- package/dist/drafting.d.ts.map +1 -0
- package/dist/drafting.js +116 -0
- package/dist/drafting.js.map +1 -0
- package/dist/gap.d.ts +27 -0
- package/dist/gap.d.ts.map +1 -0
- package/dist/gap.js +90 -0
- package/dist/gap.js.map +1 -0
- package/dist/license.d.ts +40 -0
- package/dist/license.d.ts.map +1 -0
- package/dist/license.js +122 -0
- package/dist/license.js.map +1 -0
- package/dist/llm-enhance.d.ts +69 -0
- package/dist/llm-enhance.d.ts.map +1 -0
- package/dist/llm-enhance.js +376 -0
- package/dist/llm-enhance.js.map +1 -0
- package/dist/matching/engine.d.ts +31 -0
- package/dist/matching/engine.d.ts.map +1 -0
- package/dist/matching/engine.js +51 -0
- package/dist/matching/engine.js.map +1 -0
- package/dist/matching/index.d.ts +8 -0
- package/dist/matching/index.d.ts.map +1 -0
- package/dist/matching/index.js +8 -0
- package/dist/matching/index.js.map +1 -0
- package/dist/matching/scoring.d.ts +84 -0
- package/dist/matching/scoring.d.ts.map +1 -0
- package/dist/matching/scoring.js +184 -0
- package/dist/matching/scoring.js.map +1 -0
- package/dist/models.d.ts +221 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +28 -0
- package/dist/models.js.map +1 -0
- package/dist/requirements.d.ts +22 -0
- package/dist/requirements.d.ts.map +1 -0
- package/dist/requirements.js +30 -0
- package/dist/requirements.js.map +1 -0
- package/dist/resume-intel.d.ts +40 -0
- package/dist/resume-intel.d.ts.map +1 -0
- package/dist/resume-intel.js +111 -0
- package/dist/resume-intel.js.map +1 -0
- package/dist/sources.d.ts +32 -0
- package/dist/sources.d.ts.map +1 -0
- package/dist/sources.js +72 -0
- package/dist/sources.js.map +1 -0
- package/dist/tracking.d.ts +68 -0
- package/dist/tracking.d.ts.map +1 -0
- package/dist/tracking.js +140 -0
- package/dist/tracking.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/text-processing.ts — Shared text processing utilities.
|
|
3
|
+
*
|
|
4
|
+
* Used by the matching engine (Phase 4), resume intelligence (Phase 5),
|
|
5
|
+
* and gap analysis (Phase 5). All functions are pure and stateless.
|
|
6
|
+
*
|
|
7
|
+
* Design notes:
|
|
8
|
+
* - No external NLP dependencies — stopword logic is ported directly
|
|
9
|
+
* from the Python careerclaw implementation.
|
|
10
|
+
* - STOPWORDS is the single source of truth. No parallel lists.
|
|
11
|
+
* - Phrases are sliding-window bigrams and trigrams over the token stream.
|
|
12
|
+
* - SECTION_WEIGHTS drives keyword importance in resume intelligence (Phase 5).
|
|
13
|
+
*/
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Stopwords
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
/**
|
|
18
|
+
* Combined stopword set: common English function words + recruitment
|
|
19
|
+
* boilerplate terms that carry zero signal value for job matching.
|
|
20
|
+
*
|
|
21
|
+
* Recruitment boilerplate sourced from PR-E signal hygiene analysis
|
|
22
|
+
* (February 2026): terms that appeared consistently in gap keyword lists
|
|
23
|
+
* despite having no technical meaning.
|
|
24
|
+
*/
|
|
25
|
+
export const STOPWORDS = new Set([
|
|
26
|
+
// Common English function words
|
|
27
|
+
"a", "an", "the", "and", "or", "but", "in", "on", "at", "to", "for",
|
|
28
|
+
"of", "with", "by", "from", "as", "is", "was", "are", "were", "be",
|
|
29
|
+
"been", "being", "am", "have", "has", "had", "do", "does", "did", "will",
|
|
30
|
+
"would", "could", "should", "may", "might", "shall", "can", "need",
|
|
31
|
+
"dare", "ought", "used", "it", "its", "this", "that", "these", "those",
|
|
32
|
+
"i", "you", "he", "she", "we", "they", "me", "him", "her", "us",
|
|
33
|
+
// Contractions
|
|
34
|
+
"i'm", "i've", "i'll", "i'd", "you're", "we're", "they're", "it's",
|
|
35
|
+
"don't", "doesn't", "can't", "won't", "isn't", "aren't", "wasn't", "weren't",
|
|
36
|
+
"them", "my", "your", "his", "our", "their", "what", "which", "who",
|
|
37
|
+
"whom", "when", "where", "why", "how", "all", "each", "every", "both",
|
|
38
|
+
"few", "more", "most", "other", "some", "such", "no", "not", "only",
|
|
39
|
+
"same", "so", "than", "too", "very", "just", "about", "above", "after",
|
|
40
|
+
"also", "between", "during", "into", "through", "up", "out", "if",
|
|
41
|
+
"while", "then", "than", "there", "here", "any", "now", "new", "well",
|
|
42
|
+
"back", "even", "still", "way", "take", "get", "make", "know", "time",
|
|
43
|
+
"year", "day", "part", "over", "many", "much", "own", "go", "see",
|
|
44
|
+
"come", "think", "look", "want", "give", "use", "find", "tell", "ask",
|
|
45
|
+
"work", "seem", "feel", "try", "leave", "call", "keep", "let", "begin",
|
|
46
|
+
"show", "hear", "play", "run", "move", "live", "believe", "hold",
|
|
47
|
+
"bring", "happen", "write", "provide", "sit", "stand", "lose", "pay",
|
|
48
|
+
"meet", "include", "continue", "set", "learn", "change", "lead",
|
|
49
|
+
"understand", "watch", "follow", "stop", "create", "speak", "read",
|
|
50
|
+
"spend", "grow", "open", "walk", "win", "offer", "remember", "love",
|
|
51
|
+
"consider", "appear", "buy", "wait", "serve", "die", "send", "expect",
|
|
52
|
+
"build", "stay", "fall", "cut", "reach", "kill", "remain", "suggest",
|
|
53
|
+
"raise", "pass", "sell", "require", "report", "decide", "pull", "based",
|
|
54
|
+
"able", "across", "against", "along", "already", "although", "among",
|
|
55
|
+
"around", "away", "because", "before", "below", "beside", "beyond",
|
|
56
|
+
"down", "early", "enough", "ever", "first", "further", "however", "last",
|
|
57
|
+
"late", "later", "least", "less", "like", "long", "near", "never",
|
|
58
|
+
"next", "often", "once", "our", "per", "quite", "rather", "since",
|
|
59
|
+
"soon", "still", "though", "together", "under", "until", "upon",
|
|
60
|
+
"usually", "whether", "within", "without", "yet", "second", "third",
|
|
61
|
+
// Recruitment process terms (PR-E signal hygiene)
|
|
62
|
+
"apply", "applying", "applicant", "applicants", "application",
|
|
63
|
+
"applications", "submit", "submission", "submissions",
|
|
64
|
+
"candidate", "candidates", "qualified", "successful", "shortlisted",
|
|
65
|
+
"interview", "interviewing", "hire", "hiring", "onboard", "onboarding",
|
|
66
|
+
"recruit", "recruiting", "recruitment",
|
|
67
|
+
// Generic descriptor terms (PR-E signal hygiene)
|
|
68
|
+
"competitive", "opportunity", "opportunities", "benefit", "benefits",
|
|
69
|
+
"package", "responsible", "responsibilities", "responsibility",
|
|
70
|
+
"seeking", "looking", "plus", "bonus", "nice-to-have", "preferred",
|
|
71
|
+
"required", "requirement", "requirements", "experience", "years",
|
|
72
|
+
"role", "roles", "position", "positions", "team", "teams", "company",
|
|
73
|
+
"companies", "business", "businesses", "product", "products",
|
|
74
|
+
"mission", "vision", "values", "culture", "environment", "office",
|
|
75
|
+
"full-time", "part-time", "contract", "permanent", "temporary",
|
|
76
|
+
"salary", "compensation", "equity", "stock", "options", "remote",
|
|
77
|
+
"hybrid", "onsite", "location", "worldwide", "anywhere", "global",
|
|
78
|
+
"join", "help", "love", "great", "good", "best", "strong", "deep",
|
|
79
|
+
"high", "low", "fast", "smart", "care", "important", "key", "top",
|
|
80
|
+
"amazing", "excellent", "outstanding", "exceptional", "passionate",
|
|
81
|
+
"driven", "motivated", "collaborative", "dynamic", "innovative",
|
|
82
|
+
"exciting", "unique", "diverse", "inclusive", "equal", "opportunity",
|
|
83
|
+
]);
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Section weights
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
/**
|
|
88
|
+
* Relative importance weights for resume sections in keyword extraction.
|
|
89
|
+
* Used by resume intelligence (Phase 5) to weight keyword scores.
|
|
90
|
+
* Skills = 1.0 is the maximum weight.
|
|
91
|
+
*/
|
|
92
|
+
export const SECTION_WEIGHTS = {
|
|
93
|
+
skills: 1.0,
|
|
94
|
+
summary: 0.8,
|
|
95
|
+
experience: 0.6,
|
|
96
|
+
education: 0.4,
|
|
97
|
+
other: 0.3,
|
|
98
|
+
};
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Tokenization
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
/**
|
|
103
|
+
* Tokenize a text string into a filtered array of lowercase tokens.
|
|
104
|
+
*
|
|
105
|
+
* Steps:
|
|
106
|
+
* 1. Lowercase
|
|
107
|
+
* 2. Split on non-alphanumeric boundaries (preserves hyphenated terms)
|
|
108
|
+
* 3. Strip leading/trailing punctuation from each token
|
|
109
|
+
* 4. Drop tokens shorter than MIN_TOKEN_LENGTH
|
|
110
|
+
* 5. Drop stopwords
|
|
111
|
+
*/
|
|
112
|
+
const MIN_TOKEN_LENGTH = 2;
|
|
113
|
+
export function tokenize(text) {
|
|
114
|
+
return text
|
|
115
|
+
.toLowerCase()
|
|
116
|
+
.split(/[\s,;:!?()[\]{}<>"]+/)
|
|
117
|
+
.map((t) => t.replace(/^[^a-z0-9-]+|[^a-z0-9-]+$/g, ""))
|
|
118
|
+
.filter((t) => t.length >= MIN_TOKEN_LENGTH && !STOPWORDS.has(t));
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Tokenize and return only unique tokens (set semantics, insertion order).
|
|
122
|
+
* Useful for keyword overlap scoring where duplicates add no information.
|
|
123
|
+
*/
|
|
124
|
+
export function tokenizeUnique(text) {
|
|
125
|
+
return [...new Set(tokenize(text))];
|
|
126
|
+
}
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Phrase extraction
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
/**
|
|
131
|
+
* Extract bigrams and trigrams from a token stream.
|
|
132
|
+
*
|
|
133
|
+
* Tokens are joined with a space: ["senior", "engineer"] → "senior engineer".
|
|
134
|
+
* Phrases from short token streams (< 2 tokens) are returned as empty array.
|
|
135
|
+
*/
|
|
136
|
+
export function extractPhrases(tokens) {
|
|
137
|
+
const phrases = [];
|
|
138
|
+
for (let i = 0; i < tokens.length - 1; i++) {
|
|
139
|
+
// Bigram
|
|
140
|
+
phrases.push(`${tokens[i]} ${tokens[i + 1]}`);
|
|
141
|
+
// Trigram
|
|
142
|
+
if (i < tokens.length - 2) {
|
|
143
|
+
phrases.push(`${tokens[i]} ${tokens[i + 1]} ${tokens[i + 2]}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return phrases;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Tokenize text and extract bigram + trigram phrases in one pass.
|
|
150
|
+
* Returns unique phrases (insertion order).
|
|
151
|
+
*/
|
|
152
|
+
export function extractPhrasesFromText(text) {
|
|
153
|
+
return [...new Set(extractPhrases(tokenize(text)))];
|
|
154
|
+
}
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Keyword overlap
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
/**
|
|
159
|
+
* Compute the Jaccard-like overlap between two token sets.
|
|
160
|
+
* Returns a value in [0, 1]: |intersection| / |union|.
|
|
161
|
+
*/
|
|
162
|
+
export function tokenOverlap(a, b) {
|
|
163
|
+
if (a.length === 0 && b.length === 0)
|
|
164
|
+
return 0;
|
|
165
|
+
const setA = new Set(a);
|
|
166
|
+
const setB = new Set(b);
|
|
167
|
+
const intersection = a.filter((t) => setB.has(t)).length;
|
|
168
|
+
const union = new Set([...setA, ...setB]).size;
|
|
169
|
+
return union === 0 ? 0 : intersection / union;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Return tokens in `query` that are present in `corpus`.
|
|
173
|
+
* Used to produce "matched_keywords" in ScoredJob.
|
|
174
|
+
*/
|
|
175
|
+
export function matchedTokens(query, corpus) {
|
|
176
|
+
const corpusSet = new Set(corpus);
|
|
177
|
+
return [...new Set(query.filter((t) => corpusSet.has(t)))];
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Return tokens in `query` that are absent from `corpus`.
|
|
181
|
+
* Used to produce "gap_keywords" in ScoredJob.
|
|
182
|
+
*/
|
|
183
|
+
export function gapTokens(query, corpus) {
|
|
184
|
+
const corpusSet = new Set(corpus);
|
|
185
|
+
return [...new Set(query.filter((t) => !corpusSet.has(t)))];
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=text-processing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-processing.js","sourceRoot":"","sources":["../../src/core/text-processing.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* drafting.ts — Deterministic outreach draft generator (Free tier).
|
|
3
|
+
*
|
|
4
|
+
* `draftOutreach()` produces a professional outreach email from a fixed
|
|
5
|
+
* template. It is the Free tier baseline and always runs as the fallback
|
|
6
|
+
* when LLM enhancement is unavailable or fails.
|
|
7
|
+
*
|
|
8
|
+
* Template is ported directly from the Python careerclaw implementation
|
|
9
|
+
* so output format is consistent across both runtimes.
|
|
10
|
+
*
|
|
11
|
+
* Design constraints (from MVP Feature Contract):
|
|
12
|
+
* - 150–250 word body
|
|
13
|
+
* - Professional and concise tone
|
|
14
|
+
* - Inserts job title, company name, matched skill highlights
|
|
15
|
+
* - No external dependencies, fully deterministic
|
|
16
|
+
* - llm_enhanced is always false
|
|
17
|
+
*/
|
|
18
|
+
import type { NormalizedJob, UserProfile, OutreachDraft } from "./models.js";
|
|
19
|
+
/**
|
|
20
|
+
* Generate a deterministic outreach draft for a ranked job.
|
|
21
|
+
*
|
|
22
|
+
* @param job - The job to draft for
|
|
23
|
+
* @param profile - User profile (experience_years used in body)
|
|
24
|
+
* @param matchedKeywords - Matched keywords from ScoredJob or GapAnalysis
|
|
25
|
+
* (up to 3 are highlighted in the body)
|
|
26
|
+
*/
|
|
27
|
+
export declare function draftOutreach(job: NormalizedJob, profile: UserProfile, matchedKeywords?: string[]): OutreachDraft;
|
|
28
|
+
//# sourceMappingURL=drafting.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drafting.d.ts","sourceRoot":"","sources":["../src/drafting.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAM7E;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,WAAW,EACpB,eAAe,GAAE,MAAM,EAAO,GAC7B,aAAa,CAUf"}
|
package/dist/drafting.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* drafting.ts — Deterministic outreach draft generator (Free tier).
|
|
3
|
+
*
|
|
4
|
+
* `draftOutreach()` produces a professional outreach email from a fixed
|
|
5
|
+
* template. It is the Free tier baseline and always runs as the fallback
|
|
6
|
+
* when LLM enhancement is unavailable or fails.
|
|
7
|
+
*
|
|
8
|
+
* Template is ported directly from the Python careerclaw implementation
|
|
9
|
+
* so output format is consistent across both runtimes.
|
|
10
|
+
*
|
|
11
|
+
* Design constraints (from MVP Feature Contract):
|
|
12
|
+
* - 150–250 word body
|
|
13
|
+
* - Professional and concise tone
|
|
14
|
+
* - Inserts job title, company name, matched skill highlights
|
|
15
|
+
* - No external dependencies, fully deterministic
|
|
16
|
+
* - llm_enhanced is always false
|
|
17
|
+
*/
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Public API
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
/**
|
|
22
|
+
* Generate a deterministic outreach draft for a ranked job.
|
|
23
|
+
*
|
|
24
|
+
* @param job - The job to draft for
|
|
25
|
+
* @param profile - User profile (experience_years used in body)
|
|
26
|
+
* @param matchedKeywords - Matched keywords from ScoredJob or GapAnalysis
|
|
27
|
+
* (up to 3 are highlighted in the body)
|
|
28
|
+
*/
|
|
29
|
+
export function draftOutreach(job, profile, matchedKeywords = []) {
|
|
30
|
+
const subject = buildSubject(job);
|
|
31
|
+
const body = buildBody(job, profile, matchedKeywords);
|
|
32
|
+
return {
|
|
33
|
+
job_id: job.job_id,
|
|
34
|
+
subject,
|
|
35
|
+
body,
|
|
36
|
+
llm_enhanced: false,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Template builders
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
function buildSubject(job) {
|
|
43
|
+
return `Interest in ${job.title} at ${job.company}`;
|
|
44
|
+
}
|
|
45
|
+
function buildBody(job, profile, matchedKeywords) {
|
|
46
|
+
const experienceClause = buildExperienceClause(profile);
|
|
47
|
+
const highlightsClause = buildHighlightsClause(matchedKeywords, job);
|
|
48
|
+
return [
|
|
49
|
+
`Hi ${job.company} team,`,
|
|
50
|
+
"",
|
|
51
|
+
`I'm reaching out to express interest in the ${job.title} role at ${job.company}. ` +
|
|
52
|
+
`I have ${experienceClause} building production systems end-to-end, with a consistent ` +
|
|
53
|
+
`focus on code quality, system reliability, and close collaboration with product and ` +
|
|
54
|
+
`design stakeholders. I've worked across the full lifecycle from early architecture ` +
|
|
55
|
+
`through operational ownership.`,
|
|
56
|
+
"",
|
|
57
|
+
highlightsClause,
|
|
58
|
+
"",
|
|
59
|
+
"That aligns well with my background, including:",
|
|
60
|
+
"- Delivering production-ready features with strong ownership and " +
|
|
61
|
+
"attention to reliability",
|
|
62
|
+
"- Writing clear, maintainable code and collaborating closely with " +
|
|
63
|
+
"stakeholders",
|
|
64
|
+
"- Diagnosing issues quickly and improving systems through good " +
|
|
65
|
+
"instrumentation and feedback loops",
|
|
66
|
+
"",
|
|
67
|
+
"If helpful, I can share a brief summary of relevant work and walk " +
|
|
68
|
+
"you through how I'd approach the first 30 days in this role. " +
|
|
69
|
+
"I'm happy to answer any questions about my background. " +
|
|
70
|
+
"Thanks for your time — I'd love to connect.",
|
|
71
|
+
"",
|
|
72
|
+
"Best regards,",
|
|
73
|
+
"[Your Name]",
|
|
74
|
+
].join("\n");
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Build the experience clause from profile.experience_years.
|
|
78
|
+
* Falls back to "extensive experience" when not set.
|
|
79
|
+
*/
|
|
80
|
+
function buildExperienceClause(profile) {
|
|
81
|
+
if (profile.experience_years === null || profile.experience_years === undefined) {
|
|
82
|
+
return "extensive experience";
|
|
83
|
+
}
|
|
84
|
+
return `${profile.experience_years}+ years of experience`;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Build the skill highlights sentence from matched keywords.
|
|
88
|
+
* Uses up to 3 keywords. Falls back to a generic sentence if none.
|
|
89
|
+
*/
|
|
90
|
+
function buildHighlightsClause(matchedKeywords, job) {
|
|
91
|
+
const highlights = matchedKeywords.slice(0, 3);
|
|
92
|
+
if (highlights.length === 0) {
|
|
93
|
+
return (`My background includes skills directly relevant to the ${job.title} ` +
|
|
94
|
+
`role, and I believe my experience would translate well to the ` +
|
|
95
|
+
`challenges your team is working on.`);
|
|
96
|
+
}
|
|
97
|
+
const formatted = formatList(highlights);
|
|
98
|
+
return `My background includes direct experience with ${formatted}, ` +
|
|
99
|
+
`which maps well to what you're building at ${job.company}.`;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Format a list of strings as natural language.
|
|
103
|
+
* ["a"] → "a"
|
|
104
|
+
* ["a", "b"] → "a and b"
|
|
105
|
+
* ["a", "b", "c"] → "a, b, and c"
|
|
106
|
+
*/
|
|
107
|
+
function formatList(items) {
|
|
108
|
+
if (items.length === 0)
|
|
109
|
+
return "";
|
|
110
|
+
if (items.length === 1)
|
|
111
|
+
return items[0];
|
|
112
|
+
if (items.length === 2)
|
|
113
|
+
return `${items[0]} and ${items[1]}`;
|
|
114
|
+
return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=drafting.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drafting.js","sourceRoot":"","sources":["../src/drafting.ts"],"names":[],"mappings":"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"}
|
package/dist/gap.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gap.ts — Gap analysis engine.
|
|
3
|
+
*
|
|
4
|
+
* `gapAnalysis()` compares resume intelligence against job requirements
|
|
5
|
+
* to produce:
|
|
6
|
+
* - fit_score (weighted: rewards high-weight resume keywords)
|
|
7
|
+
* - fit_score_unweighted (Jaccard overlap — raw coverage)
|
|
8
|
+
* - signals (resume keywords/phrases present in the job)
|
|
9
|
+
* - gaps (job keywords/phrases absent from the resume)
|
|
10
|
+
* - summary (top-5 of each for display)
|
|
11
|
+
*
|
|
12
|
+
* This is a Pro tier feature. It is called after `rankJobs()` and its
|
|
13
|
+
* output supplements the `ScoredJob` with resume-specific insight.
|
|
14
|
+
*
|
|
15
|
+
* Fit score interpretation (from README):
|
|
16
|
+
* >= 40% — strong match
|
|
17
|
+
* Practical ceiling ~50% due to company/location tokens in denominator
|
|
18
|
+
*/
|
|
19
|
+
import type { NormalizedJob, ResumeIntelligence, GapAnalysisResult } from "./models.js";
|
|
20
|
+
/**
|
|
21
|
+
* Run gap analysis between resume intelligence and a job posting.
|
|
22
|
+
*
|
|
23
|
+
* @param intel - Output of `buildResumeIntelligence()`
|
|
24
|
+
* @param job - The job to analyse against
|
|
25
|
+
*/
|
|
26
|
+
export declare function gapAnalysis(intel: ResumeIntelligence, job: NormalizedJob): GapAnalysisResult;
|
|
27
|
+
//# sourceMappingURL=gap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gap.d.ts","sourceRoot":"","sources":["../src/gap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAaxF;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,kBAAkB,EACzB,GAAG,EAAE,aAAa,GACjB,iBAAiB,CA0DnB"}
|
package/dist/gap.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gap.ts — Gap analysis engine.
|
|
3
|
+
*
|
|
4
|
+
* `gapAnalysis()` compares resume intelligence against job requirements
|
|
5
|
+
* to produce:
|
|
6
|
+
* - fit_score (weighted: rewards high-weight resume keywords)
|
|
7
|
+
* - fit_score_unweighted (Jaccard overlap — raw coverage)
|
|
8
|
+
* - signals (resume keywords/phrases present in the job)
|
|
9
|
+
* - gaps (job keywords/phrases absent from the resume)
|
|
10
|
+
* - summary (top-5 of each for display)
|
|
11
|
+
*
|
|
12
|
+
* This is a Pro tier feature. It is called after `rankJobs()` and its
|
|
13
|
+
* output supplements the `ScoredJob` with resume-specific insight.
|
|
14
|
+
*
|
|
15
|
+
* Fit score interpretation (from README):
|
|
16
|
+
* >= 40% — strong match
|
|
17
|
+
* Practical ceiling ~50% due to company/location tokens in denominator
|
|
18
|
+
*/
|
|
19
|
+
import { extractJobRequirements } from "./requirements.js";
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Constants
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
const TOP_N = 5;
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Public API
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
/**
|
|
28
|
+
* Run gap analysis between resume intelligence and a job posting.
|
|
29
|
+
*
|
|
30
|
+
* @param intel - Output of `buildResumeIntelligence()`
|
|
31
|
+
* @param job - The job to analyse against
|
|
32
|
+
*/
|
|
33
|
+
export function gapAnalysis(intel, job) {
|
|
34
|
+
const requirements = extractJobRequirements(job);
|
|
35
|
+
const jobKeywordSet = new Set(requirements.keywords);
|
|
36
|
+
const jobPhraseSet = new Set(requirements.phrases);
|
|
37
|
+
const resumeKeywordSet = new Set(intel.extracted_keywords);
|
|
38
|
+
const resumePhraseSet = new Set(intel.extracted_phrases);
|
|
39
|
+
// ---- Signals (resume ∩ job) ----
|
|
40
|
+
const signalKeywords = intel.extracted_keywords.filter((k) => jobKeywordSet.has(k));
|
|
41
|
+
const signalPhrases = intel.extracted_phrases.filter((p) => jobPhraseSet.has(p));
|
|
42
|
+
// ---- Gaps (job − resume) ----
|
|
43
|
+
const gapKeywords = requirements.keywords.filter((k) => !resumeKeywordSet.has(k));
|
|
44
|
+
const gapPhrases = requirements.phrases.filter((p) => !resumePhraseSet.has(p));
|
|
45
|
+
// ---- Fit score (weighted) ----
|
|
46
|
+
// Sum the keyword_weight of each matched keyword / total job keyword count
|
|
47
|
+
const jobKeywordCount = requirements.keywords.length;
|
|
48
|
+
let weightedMatchSum = 0;
|
|
49
|
+
for (const kw of signalKeywords) {
|
|
50
|
+
weightedMatchSum += intel.keyword_weights[kw] ?? 0;
|
|
51
|
+
}
|
|
52
|
+
const fit_score = jobKeywordCount > 0
|
|
53
|
+
? roundScore(weightedMatchSum / jobKeywordCount)
|
|
54
|
+
: 0;
|
|
55
|
+
// ---- Fit score (unweighted / Jaccard) ----
|
|
56
|
+
const unionSize = new Set([
|
|
57
|
+
...intel.extracted_keywords,
|
|
58
|
+
...requirements.keywords,
|
|
59
|
+
]).size;
|
|
60
|
+
const fit_score_unweighted = unionSize > 0 ? roundScore(signalKeywords.length / unionSize) : 0;
|
|
61
|
+
return {
|
|
62
|
+
fit_score,
|
|
63
|
+
fit_score_unweighted,
|
|
64
|
+
signals: {
|
|
65
|
+
keywords: signalKeywords,
|
|
66
|
+
phrases: signalPhrases,
|
|
67
|
+
},
|
|
68
|
+
gaps: {
|
|
69
|
+
keywords: gapKeywords,
|
|
70
|
+
phrases: gapPhrases,
|
|
71
|
+
},
|
|
72
|
+
summary: {
|
|
73
|
+
top_signals: {
|
|
74
|
+
keywords: signalKeywords.slice(0, TOP_N),
|
|
75
|
+
phrases: signalPhrases.slice(0, TOP_N),
|
|
76
|
+
},
|
|
77
|
+
top_gaps: {
|
|
78
|
+
keywords: gapKeywords.slice(0, TOP_N),
|
|
79
|
+
phrases: gapPhrases.slice(0, TOP_N),
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Helpers
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
function roundScore(n) {
|
|
88
|
+
return Math.round(n * 10_000) / 10_000;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=gap.js.map
|
package/dist/gap.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gap.js","sourceRoot":"","sources":["../src/gap.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* license.ts — Gumroad Pro license validation with offline cache.
|
|
3
|
+
*
|
|
4
|
+
* `checkLicense(key, options?)` validates a CareerClaw Pro license key
|
|
5
|
+
* against the Gumroad API and caches the result for 7 days so the tool
|
|
6
|
+
* works normally during short network outages.
|
|
7
|
+
*
|
|
8
|
+
* Design principles:
|
|
9
|
+
* - Raw key NEVER written to disk — only sha256(key) is cached.
|
|
10
|
+
* - Graceful degradation: if Gumroad is unreachable and the cache is
|
|
11
|
+
* fresh (< LICENSE_CACHE_TTL_MS), the cached result is trusted.
|
|
12
|
+
* - Free tier safe: if GUMROAD_PRODUCT_ID is not configured, returns
|
|
13
|
+
* { valid: false, source: "none" } immediately without erroring.
|
|
14
|
+
* - Testable: fetchFn and cachePath are injectable for offline tests.
|
|
15
|
+
*/
|
|
16
|
+
export interface LicenseResult {
|
|
17
|
+
valid: boolean;
|
|
18
|
+
/** Where the result came from. */
|
|
19
|
+
source: "api" | "cache" | "none";
|
|
20
|
+
}
|
|
21
|
+
export interface CheckLicenseOptions {
|
|
22
|
+
/** Injectable fetch — defaults to global fetch. */
|
|
23
|
+
fetchFn?: typeof fetch;
|
|
24
|
+
/** Override cache file path — for tests using a tmpdir. */
|
|
25
|
+
cachePath?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Validate a CareerClaw Pro license key.
|
|
29
|
+
*
|
|
30
|
+
* Resolution order:
|
|
31
|
+
* 1. If GUMROAD_PRODUCT_ID is unset → { valid: false, source: "none" }
|
|
32
|
+
* 2. POST to Gumroad API (increment_uses_count=false)
|
|
33
|
+
* - success + not refunded + not chargebacked → cache hash, return api
|
|
34
|
+
* - refunded / chargebacked / not found → { valid: false, source: "api" }
|
|
35
|
+
* 3. On network failure → read cache
|
|
36
|
+
* - hash matches + age < TTL → { valid: true, source: "cache" }
|
|
37
|
+
* - stale, missing, or hash mismatch → { valid: false, source: "none" }
|
|
38
|
+
*/
|
|
39
|
+
export declare function checkLicense(key: string, options?: CheckLicenseOptions): Promise<LicenseResult>;
|
|
40
|
+
//# sourceMappingURL=license.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"license.d.ts","sourceRoot":"","sources":["../src/license.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAiBH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,OAAO,CAAC;IACf,kCAAkC;IAClC,MAAM,EAAE,KAAK,GAAG,OAAO,GAAG,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,mBAAmB;IAClC,mDAAmD;IACnD,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAaD;;;;;;;;;;;GAWG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,aAAa,CAAC,CAoBxB"}
|
package/dist/license.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* license.ts — Gumroad Pro license validation with offline cache.
|
|
3
|
+
*
|
|
4
|
+
* `checkLicense(key, options?)` validates a CareerClaw Pro license key
|
|
5
|
+
* against the Gumroad API and caches the result for 7 days so the tool
|
|
6
|
+
* works normally during short network outages.
|
|
7
|
+
*
|
|
8
|
+
* Design principles:
|
|
9
|
+
* - Raw key NEVER written to disk — only sha256(key) is cached.
|
|
10
|
+
* - Graceful degradation: if Gumroad is unreachable and the cache is
|
|
11
|
+
* fresh (< LICENSE_CACHE_TTL_MS), the cached result is trusted.
|
|
12
|
+
* - Free tier safe: if GUMROAD_PRODUCT_ID is not configured, returns
|
|
13
|
+
* { valid: false, source: "none" } immediately without erroring.
|
|
14
|
+
* - Testable: fetchFn and cachePath are injectable for offline tests.
|
|
15
|
+
*/
|
|
16
|
+
import { createHash } from "node:crypto";
|
|
17
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
18
|
+
import { dirname } from "node:path";
|
|
19
|
+
import { GUMROAD_API_BASE, GUMROAD_PRODUCT_ID, LICENSE_CACHE_PATH, LICENSE_CACHE_TTL_MS, HTTP_TIMEOUT_MS, } from "./config.js";
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Public API
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
/**
|
|
24
|
+
* Validate a CareerClaw Pro license key.
|
|
25
|
+
*
|
|
26
|
+
* Resolution order:
|
|
27
|
+
* 1. If GUMROAD_PRODUCT_ID is unset → { valid: false, source: "none" }
|
|
28
|
+
* 2. POST to Gumroad API (increment_uses_count=false)
|
|
29
|
+
* - success + not refunded + not chargebacked → cache hash, return api
|
|
30
|
+
* - refunded / chargebacked / not found → { valid: false, source: "api" }
|
|
31
|
+
* 3. On network failure → read cache
|
|
32
|
+
* - hash matches + age < TTL → { valid: true, source: "cache" }
|
|
33
|
+
* - stale, missing, or hash mismatch → { valid: false, source: "none" }
|
|
34
|
+
*/
|
|
35
|
+
export async function checkLicense(key, options = {}) {
|
|
36
|
+
const fetchFn = options.fetchFn ?? fetch;
|
|
37
|
+
const cachePath = options.cachePath ?? LICENSE_CACHE_PATH;
|
|
38
|
+
// Guard: product ID must be configured
|
|
39
|
+
if (!GUMROAD_PRODUCT_ID) {
|
|
40
|
+
return { valid: false, source: "none" };
|
|
41
|
+
}
|
|
42
|
+
// Attempt live API validation
|
|
43
|
+
try {
|
|
44
|
+
const result = await verifyWithApi(key, GUMROAD_PRODUCT_ID, fetchFn);
|
|
45
|
+
if (result.valid) {
|
|
46
|
+
writeCache(key, cachePath);
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Network failure — fall back to cache
|
|
52
|
+
return readCache(key, cachePath);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function verifyWithApi(key, productId, fetchFn) {
|
|
56
|
+
const body = new URLSearchParams({
|
|
57
|
+
product_id: productId,
|
|
58
|
+
license_key: key,
|
|
59
|
+
increment_uses_count: "false",
|
|
60
|
+
});
|
|
61
|
+
const res = await fetchFn(`${GUMROAD_API_BASE}/v2/licenses/verify`, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
64
|
+
body: body.toString(),
|
|
65
|
+
signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
|
|
66
|
+
});
|
|
67
|
+
// Gumroad returns 404 for invalid/unknown keys
|
|
68
|
+
if (res.status === 404) {
|
|
69
|
+
return { valid: false, source: "api" };
|
|
70
|
+
}
|
|
71
|
+
if (!res.ok) {
|
|
72
|
+
// Non-404 HTTP errors (5xx, etc.) are treated as network failures
|
|
73
|
+
throw new Error(`Gumroad HTTP ${res.status}`);
|
|
74
|
+
}
|
|
75
|
+
const data = (await res.json());
|
|
76
|
+
if (!data.success) {
|
|
77
|
+
return { valid: false, source: "api" };
|
|
78
|
+
}
|
|
79
|
+
if (data.purchase?.refunded || data.purchase?.chargebacked) {
|
|
80
|
+
return { valid: false, source: "api" };
|
|
81
|
+
}
|
|
82
|
+
return { valid: true, source: "api" };
|
|
83
|
+
}
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Cache helpers
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
function hashKey(key) {
|
|
88
|
+
return createHash("sha256").update(key).digest("hex");
|
|
89
|
+
}
|
|
90
|
+
function writeCache(key, cachePath) {
|
|
91
|
+
try {
|
|
92
|
+
mkdirSync(dirname(cachePath), { recursive: true });
|
|
93
|
+
const cache = {
|
|
94
|
+
key_hash: hashKey(key),
|
|
95
|
+
validated_at: new Date().toISOString(),
|
|
96
|
+
};
|
|
97
|
+
writeFileSync(cachePath, JSON.stringify(cache, null, 2), "utf8");
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Cache write failure is non-fatal — validation already succeeded
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function readCache(key, cachePath) {
|
|
104
|
+
try {
|
|
105
|
+
const raw = readFileSync(cachePath, "utf8");
|
|
106
|
+
const cache = JSON.parse(raw);
|
|
107
|
+
// Hash must match the key being checked
|
|
108
|
+
if (cache.key_hash !== hashKey(key)) {
|
|
109
|
+
return { valid: false, source: "none" };
|
|
110
|
+
}
|
|
111
|
+
// Cache must be within the TTL window
|
|
112
|
+
const age = Date.now() - new Date(cache.validated_at).getTime();
|
|
113
|
+
if (age > LICENSE_CACHE_TTL_MS) {
|
|
114
|
+
return { valid: false, source: "none" };
|
|
115
|
+
}
|
|
116
|
+
return { valid: true, source: "cache" };
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return { valid: false, source: "none" };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=license.js.map
|