@timurtekb/tekjobs 0.43.0 → 0.44.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 +6 -0
- package/app/dist/assets/{index-CHQPdodQ.css → index-CIqEcxvl.css} +1 -1
- package/app/dist/assets/{index-C77HG2Sx.js → index-D9GAewm_.js} +4 -4
- package/app/dist/index.html +2 -2
- package/app/server/index.mjs +2 -0
- package/app/server/mcp.mjs +6 -2
- package/app/server/store.mjs +30 -0
- package/package.json +1 -1
- package/scraper/profile.mjs +1 -1
package/app/dist/index.html
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
<link rel="icon" href="/brand/tekjobs-avatar-192.png" type="image/png" sizes="192x192" />
|
|
9
9
|
<link rel="apple-touch-icon" href="/brand/tekjobs-avatar-192.png" />
|
|
10
10
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:wght@500;600&family=Hanken+Grotesk:wght@400;500;600&family=Azeret+Mono:wght@400;500&display=swap" data-zengin="fonts" />
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-D9GAewm_.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CIqEcxvl.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
package/app/server/index.mjs
CHANGED
|
@@ -70,6 +70,8 @@ const routes = [
|
|
|
70
70
|
['GET', /^\/api\/criteria$/, () => store.getCriteria()],
|
|
71
71
|
['PUT', /^\/api\/criteria$/, async (_, __, req) => { const b = await readBody(req); return store.setCriteria(b.raw); }],
|
|
72
72
|
['GET', /^\/api\/profile\/summary$/, () => store.profileSummary()],
|
|
73
|
+
// The one-page confirmation after the interview: roles, where, pay, exclusions, proof points, what remains.
|
|
74
|
+
['GET', /^\/api\/learned$/, () => store.learned()],
|
|
73
75
|
['GET', /^\/api\/companies$/, () => store.companies()],
|
|
74
76
|
['GET', /^\/api\/feeds$/, () => store.feeds()],
|
|
75
77
|
['GET', /^\/api\/companies\/find$/, (_, q) => store.findBoard({ slug: q.get('slug') || '', ats: q.get('ats') || '', name: q.get('name') || '' })],
|
package/app/server/mcp.mjs
CHANGED
|
@@ -52,7 +52,7 @@ export const TOOLS = [
|
|
|
52
52
|
{ name: 'set_criteria', description: 'Replace the criteria JSON block. Pass the full JSON as a string; it is validated before writing.', inputSchema: { type: 'object', properties: { raw: { type: 'string' } }, required: ['raw'] } },
|
|
53
53
|
{ name: 'list_companies', description: 'The company watchlist rows (name, ats, slug, tier, status) with each board\'s health: state (failed, zero, stale, never, ok), last success, last attempt, last error.', inputSchema: { type: 'object', properties: {} } },
|
|
54
54
|
{ name: 'list_feeds', description: 'The aggregator feeds: whether each is on under the criteria\'s openSources, and how it did on the last scan.', inputSchema: { type: 'object', properties: {} } },
|
|
55
|
-
{ name: 'onboarding_status', description: 'What the onboarding still needs, in two groups: `steps` for search ready (profile folder, resume, profile note, criteria, first scan; `complete` when all done) and `writing` for writing ready (resume of record note, Positioning, Voice; `writingReady` when all done). Call this first in a new setup, and once more at the end to
|
|
55
|
+
{ name: 'onboarding_status', description: 'What the onboarding still needs, in two groups: `steps` for search ready (profile folder, resume, profile note, criteria, first scan; `complete` when all done) and `writing` for writing ready (resume of record note, Positioning, Voice; `writingReady` when all done). Once the profile is written it also carries `learned`: the roles and the title terms that find them, where, the pay floor and stretch, the exclusions, the proof points, and what remains. Call this first in a new setup, and once more at the end: read `learned` back to the person and ask what is wrong.', inputSchema: { type: 'object', properties: {} } },
|
|
56
56
|
{ name: 'onboarding_materials', description: 'Start the onboarding interview. Returns the resume text, the current profile note, the current criteria JSON, and the interview script to follow step by step. Follow the script; it ends with save_profile, set_criteria and a scan.', inputSchema: { type: 'object', properties: {} } },
|
|
57
57
|
{ name: 'save_profile', description: 'Write one profile note in full: Profile/Profile.md by default (markdown with the standard headings), or note=positioning for Profile/Positioning.md (which story leads, the evidence rule, what must never be claimed) or note=voice for Profile/Voice.md (writing rules and one letter they would send). The previous version is kept beside it.', inputSchema: { type: 'object', properties: { markdown: { type: 'string' }, note: { type: 'string', enum: ['profile', 'positioning', 'voice'], description: 'which note; default profile' } }, required: ['markdown'] } },
|
|
58
58
|
{ name: 'fetch_link', description: 'Fetch a public page the user gave you (portfolio, GitHub, personal site) as plain text, to fold into the profile. http(s) only; never use it on LinkedIn profile pages.', inputSchema: { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] } },
|
|
@@ -65,7 +65,11 @@ export const TOOLS = [
|
|
|
65
65
|
|
|
66
66
|
export async function call(name, a = {}) {
|
|
67
67
|
switch (name) {
|
|
68
|
-
case 'onboarding_status':
|
|
68
|
+
case 'onboarding_status': {
|
|
69
|
+
// Once the interview has written the profile, the status carries what was learned, so the client can read it back.
|
|
70
|
+
const s = store.onboardingStatus();
|
|
71
|
+
return s.steps.some((x) => x.id === 'profile' && x.done) ? { ...s, learned: store.learned() } : s;
|
|
72
|
+
}
|
|
69
73
|
case 'onboarding_materials': return store.onboardingMaterials();
|
|
70
74
|
case 'save_profile': return store.saveProfile(a.markdown, undefined, { note: a.note || 'profile' });
|
|
71
75
|
case 'fetch_link': return store.fetchLink(a.url);
|
package/app/server/store.mjs
CHANGED
|
@@ -1114,6 +1114,36 @@ export function profileSummary() {
|
|
|
1114
1114
|
return { exists: !!md, status: fm.status || '', updated: fm.updated || '', basics, summary: summaryText, targets, constraints, proofPoints, documents, attention, feeds, resumeAgeDays: resumeAge };
|
|
1115
1115
|
}
|
|
1116
1116
|
|
|
1117
|
+
/**
|
|
1118
|
+
* What TekJobs learned from the interview, as one page the person can check: the roles and the title terms that
|
|
1119
|
+
* find them, where, the pay floor and stretch, what is excluded, the proof points letters will lean on, and what
|
|
1120
|
+
* is still missing. Built from the profile note and the criteria each time; nothing is stored twice.
|
|
1121
|
+
*/
|
|
1122
|
+
export function learned() {
|
|
1123
|
+
const s = profileSummary();
|
|
1124
|
+
const c = safe(() => loadCriteria(), {}) || {};
|
|
1125
|
+
const byLabel = (label) => s.constraints.find((x) => x.label === label) || { profile: '', criteria: '' };
|
|
1126
|
+
const titleTerms = Object.entries(c.titleTerms || {}).sort((a, b) => b[1] - a[1]).map(([term, weight]) => ({ term, weight }));
|
|
1127
|
+
const ob = onboardingStatus();
|
|
1128
|
+
const remaining = [
|
|
1129
|
+
...(ob.writing || []).filter((w) => !w.done).map((w) => w.label),
|
|
1130
|
+
...s.attention.filter((a) => a.level === 'missing').map((a) => a.text),
|
|
1131
|
+
].slice(0, 8);
|
|
1132
|
+
return {
|
|
1133
|
+
ready: s.exists && s.status !== 'draft' && titleTerms.length > 0,
|
|
1134
|
+
name: s.basics.name, location: s.basics.location, currentRole: s.basics.currentRole, summary: s.summary,
|
|
1135
|
+
roles: s.targets.map((t) => ({ tier: t.tier, title: t.title, term: t.term })),
|
|
1136
|
+
titleTerms: titleTerms.slice(0, 10),
|
|
1137
|
+
where: { profile: byLabel('Work mode').profile, requireRemote: !!c.location?.requireRemote, metro: (c.location?.bayAreaTerms || []).slice(0, 3) },
|
|
1138
|
+
pay: { floor: c.salary?.minAnnual ?? null, stretch: c.salary?.stretchAnnual ?? null, profile: byLabel('Pay floor').profile },
|
|
1139
|
+
exclusions: { titles: c.titleExclude || [], companies: c.companyExclude || [], industries: byLabel('Industries to avoid').profile },
|
|
1140
|
+
keywords: Object.entries(c.descTerms || {}).sort((a, b) => b[1] - a[1]).slice(0, 12).map(([k]) => k),
|
|
1141
|
+
proofPoints: [...s.proofPoints].sort((a, b) => Number(b.hasNumber) - Number(a.hasNumber)).slice(0, 5).map((p) => p.text),
|
|
1142
|
+
minScore: c.minScore ?? null,
|
|
1143
|
+
searchReady: ob.complete, writingReady: ob.writingReady, remaining,
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1117
1147
|
export function saveProfileNote(key, markdown) {
|
|
1118
1148
|
const note = PROFILE_NOTES.find((n) => n.key === key);
|
|
1119
1149
|
if (!note) throw Object.assign(new Error(`No profile note "${key}"`), { status: 404 });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timurtekb/tekjobs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"description": "A local-first job-search machine: watches hundreds of company job boards, scores every posting against your profile, and files matches into a folder of markdown notes you own. Bring your own LLM through MCP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/scraper/profile.mjs
CHANGED
|
@@ -173,7 +173,7 @@ const INTERVIEW_SCRIPT = `You are onboarding a job seeker into TekJobs. Goal: wr
|
|
|
173
173
|
- location.requireRemote true only if the user said remote only; add their metro to bayAreaTerms with bayAreaBoost 12 if hybrid there is acceptable.
|
|
174
174
|
Call set_criteria with the full JSON string. It is validated before writing.
|
|
175
175
|
5. Call run_scan with dry=true and poll scan_status every few seconds until running is false (two to three minutes; the scan runs on its own, so if this session ends first, the next one can pick up with scan_status). If you cannot poll between turns, pass wait=240 to run_scan and it answers when the scan is done. A dry run writes no notes, so do not use search_jobs yet: call scan_preview limit=15 and show the user the top matches with score, pay band and one reason each. Ask whether the list looks right. Adjust the criteria once if it does not (set_criteria, then another dry run_scan and scan_preview). When it does, call run_scan with dry=false and poll scan_status until it finishes; that run writes the notes, and search_jobs and the app's Today page work from then on.
|
|
176
|
-
6.
|
|
176
|
+
6. Call onboarding_status once more. Read its "learned" block back to the user as a short confirmation in their own terms: the roles and the title terms that find them (flag any role with no term), where, the floor and the stretch, what is excluded, the proof points letters will lean on, and what remains. Ask what is wrong and fix it with save_profile or set_criteria. Then say plainly which of the two contracts it reports, search ready and writing ready, tell them where things live (Profile/Profile.md, Positioning.md, Voice.md, Targets/Search Criteria.md, Jobs/), and remind them the daily scan runs on its own from here.`;
|
|
177
177
|
|
|
178
178
|
function criteriaNote(json) {
|
|
179
179
|
return `---
|